优化
This commit is contained in:
@@ -46,7 +46,7 @@ export async function run(context) {
|
||||
| 公共主数据与往来单位 | `master-data.mjs` | `master-data-partner-contract.test.mjs` |
|
||||
| 订单创建、可计价主数据与审核异步收敛 | `orders.mjs` | `orders-pricing-fixture-contract.test.mjs`、`orders-approval-convergence-contract.test.mjs` |
|
||||
| 入库测量、首次打印冻结箱码与墨家扫码 | `inbound-measurement.mjs` | `inbound-measurement-box-code-contract.test.mjs` |
|
||||
| 三期模拟柜关联、柜次概览、渠道隔离、装柜模型桥接、PDA、报关与证据/出仓门禁 | `phase3-container-loading.mjs`、`ocean-booking-fixture.mjs`、`phase3-container-overview.mjs`、`phase3-loading.mjs`、`phase3-loading-model-bridge.mjs`、`phase3-pda-convergence.mjs`、`phase3-evidence-gate.mjs`、`phase3-customs.mjs`、`phase3-loading-finalize.mjs`、`phase3-outbound-dispatch.mjs` | `container-simulation-association-contract.test.mjs`、`ocean-booking-channel-contract.test.mjs`、`phase3-container-regression-contract.test.mjs`、`phase3-loading-model-bridge-contract.test.mjs`、`phase3-producer-chain-contract.test.mjs`、`manual-state-loading-pda-contract.test.mjs` |
|
||||
| 三期模拟柜关联、柜次概览、渠道隔离、装柜模型桥接、PDA、报关与证据/出仓门禁 | `phase3-container-loading.mjs`、`ocean-booking-fixture.mjs`、`phase3-container-overview.mjs`、`phase3-loading.mjs`、`phase3-loading-model-bridge.mjs`、`phase3-pda-convergence.mjs`、`phase3-evidence-gate.mjs`、`phase3-customs.mjs`、`phase3-loading-finalize.mjs`、`phase3-outbound-dispatch.mjs` | `container-simulation-association-contract.test.mjs`、`container-specification-fixture-contract.test.mjs`、`ocean-booking-channel-contract.test.mjs`、`phase3-container-regression-contract.test.mjs`、`phase3-loading-model-bridge-contract.test.mjs`、`phase3-producer-chain-contract.test.mjs`、`manual-state-loading-pda-contract.test.mjs` |
|
||||
| 分柜与父子入库单数量守恒 | `phase3-order-splitting.mjs` | `phase3-order-splitting-contract.test.mjs` |
|
||||
| 订舱共享主数据前置与订舱验收 | `ocean-booking-fixture.mjs`、`ocean-bookings.mjs`、`phase3-port-schedules.mjs`、`phase3-carrier-schedule-queries.mjs` | `ocean-booking-prototype-contract.test.mjs`、`ocean-booking-fixture-contract.test.mjs`、`phase3-carrier-schedule-queries.test.mjs` |
|
||||
| 拖车预约 | `truck-appointments.mjs` | `truck-appointments-contract.test.mjs` |
|
||||
|
||||
@@ -31,28 +31,42 @@ test('simulation creation uses server-owned identity and formal master data', ()
|
||||
assert.match(firstlegOpenApi, /服务端生成稳定 containerBookingId\/cabinetNo,不要求真实柜号或订舱/)
|
||||
})
|
||||
|
||||
test('first link and unlink gates are intentionally asymmetric', () => {
|
||||
test('link consumes one active real booking container through the frozen request', () => {
|
||||
assert.match(controller, /@PostMapping\("\/\{id\}\/ocean-booking-link"\)/)
|
||||
assert.match(controller, /@DeleteMapping\("\/\{id\}\/ocean-booking-link"\)/)
|
||||
assert.match(containerDtos, /OceanBookingLinkRequest\(@NotNull @Positive Long realContainerBookingId,[\s\S]*expectedVersion/)
|
||||
assert.match(containerService, /requireFirstLinkMutable\(container\)/)
|
||||
assert.match(containerService, /WAIT_LOADING && value != SmartLoadingStatus\.EDITING/)
|
||||
assert.match(containerService, /repository\.findForUpdate\(request\.realContainerBookingId\(\)\)/)
|
||||
assert.match(containerService, /"OCEAN_BOOKING"\.equals\(source\.getSourceType\(\)\)/)
|
||||
assert.match(containerService, /"ACTIVE"\.equals\(source\.getRecordStatus\(\)\)/)
|
||||
assert.match(containerService, /"CONFIRMED"\.equals\(booking\.getStatus\(\)\)[\s\S]*"RELEASED"\.equals\(booking\.getStatus\(\)\)/)
|
||||
assert.match(containerService, /hasPickTask[\s\S]*hasEvidence[\s\S]*hasDocuments/)
|
||||
assert.match(containerService, /requireUnlinkMutable\(container\)/)
|
||||
assert.match(containerService, /status\(container\) != SmartLoadingStatus\.WAIT_LOADING \|\| repository\.hasActiveLoadingItems/)
|
||||
assert.match(firstlegOpenApi, /首次关联允许 WAIT_LOADING 或已有预排货的 EDITING 柜/)
|
||||
assert.match(firstlegOpenApi, /解绑或换绑仅允许完全空的 WAIT_LOADING 模拟柜/)
|
||||
assert.match(firstlegOpenApi, /realContainerBookingId/)
|
||||
assert.match(firstlegOpenApi, /原真实柜置 MERGED 且不再出现在正常货柜列表/)
|
||||
})
|
||||
|
||||
test('real booking association locks scope capacity and stable internal identity', () => {
|
||||
test('real booking merge locks scope and changes all identities atomically', () => {
|
||||
for (const field of ['warehouseId', 'channelCode', 'containerType', 'capacityCbm', 'maxWeightKg']) {
|
||||
assert.match(containerService, new RegExp(`container\\.get${field[0].toUpperCase()}${field.slice(1)}\\(\\)`), field)
|
||||
}
|
||||
assert.match(containerService, /hasMismatchedCustomerItems/)
|
||||
assert.match(containerService, /真实订舱柜箱型分配已关联/)
|
||||
assert.match(containerService, /setOceanBookingId[\s\S]*setOceanBookingRequirementId/)
|
||||
assert.match(containerService, /consumeRealSource\(source\.getId\(\), id/)
|
||||
assert.match(containerService, /mergeRealSource\(id, source\.getId\(\), source\.getContainerNo\(\)/)
|
||||
assert.match(containerService, /reassignAllocation\(source\.getId\(\), id\)/)
|
||||
assert.doesNotMatch(containerService.match(/linkOceanBooking[\s\S]*?return required\(id\);/)?.[0] || '', /container\.setId\(|container\.setCabinetNo\(/)
|
||||
})
|
||||
|
||||
test('booking creation atomically registers one real operational container per input row', () => {
|
||||
assert.match(oceanOpenApi, /containers: \{type: array, minItems: 1, maxItems: 20/)
|
||||
assert.match(oceanOpenApi, /required: \[containerNo, containerType, containerKind, capacityCbm, maxWeightKg\]/)
|
||||
assert.match(oceanService, /request\.data\(\)\.containers\(\)\.forEach/)
|
||||
assert.match(oceanService, /同一订舱内集装箱号不能重复/)
|
||||
assert.match(oceanService, /createRealContainers\(value, requirements, request\.data\(\)\.containers\(\)/)
|
||||
assert.match(oceanService, /setSourceType\("OCEAN_BOOKING"\)/)
|
||||
assert.match(oceanService, /setRecordStatus\("ACTIVE"\)/)
|
||||
assert.match(oceanService, /setQuantity\(1\)/)
|
||||
})
|
||||
|
||||
test('HTTP fixture follows simulation arrange link PDA sequence without auto-container fallback', () => {
|
||||
const ordered = [
|
||||
'新增内部模拟柜',
|
||||
@@ -64,9 +78,10 @@ test('HTTP fixture follows simulation arrange link PDA sequence without auto-con
|
||||
assert.ok(ordered.every((value, index) => index === 0 || value > ordered[index - 1]), `invalid sequence: ${ordered.join(',')}`)
|
||||
assert.match(loadingFixture, /ocean-booking-link/)
|
||||
assert.match(loadingFixture, /关联前后内部柜次、计划和排货明细身份稳定/)
|
||||
assert.match(loadingFixture, /同一真实订舱箱型容量拒绝重复关联/)
|
||||
assert.match(loadingFixture, /已消费 MERGED 真实柜拒绝再次关联/)
|
||||
assert.match(loadingFixture, /已有排货后拒绝解绑或换绑/)
|
||||
assert.match(loadingFixture, /空模拟柜解绑后允许重新关联/)
|
||||
assert.match(loadingFixture, /合并后列表仅保留模拟柜原地转成的一条 ACTIVE 操作身份/)
|
||||
assert.match(loadingFixture, /订舱真实柜 allocation 已原子改指稳定模拟柜身份/)
|
||||
assert.doesNotMatch(loadingFixture, /确认订舱并自动生成|确认响应包含自动生成/)
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { test } from 'node:test'
|
||||
|
||||
const productionFixtures = [
|
||||
'manual-state-loading-pda.mjs',
|
||||
'ocean-booking-fixture.mjs',
|
||||
'ocean-bookings.mjs',
|
||||
'phase3-carrier-schedule-queries.mjs',
|
||||
'phase3-container-loading.mjs',
|
||||
'phase3-loading.mjs',
|
||||
'phase3-order-splitting.mjs',
|
||||
'phase3-port-schedules.mjs',
|
||||
]
|
||||
|
||||
function source(name) {
|
||||
return readFileSync(new URL(name, import.meta.url), 'utf8')
|
||||
}
|
||||
|
||||
test('HTTP fixtures use the canonical primary and mismatch container specifications', () => {
|
||||
const primarySources = productionFixtures.map(source).join('\n')
|
||||
const containerFlow = source('phase3-container-loading.mjs')
|
||||
|
||||
assert.match(primarySources, /containerType: '40HC'[\s\S]*?capacityCbm: 76[\s\S]*?maxWeightKg: 28000/)
|
||||
assert.match(containerFlow, /containerType: '20GP'[\s\S]*?capacityCbm: 33[\s\S]*?maxWeightKg: 28000/)
|
||||
})
|
||||
|
||||
test('HTTP fixtures do not reintroduce retired aliases or arbitrary specifications', () => {
|
||||
for (const name of productionFixtures) {
|
||||
const fixture = source(name)
|
||||
assert.doesNotMatch(fixture, /40HQ|45HQ/, name)
|
||||
assert.doesNotMatch(fixture, /capacityCbm:\s*(?:100|68|28)(?:\D|$)/, name)
|
||||
assert.doesNotMatch(fixture, /maxWeightKg:\s*(?:100000|22000)(?:\D|$)/, name)
|
||||
}
|
||||
})
|
||||
|
||||
test('the frozen catalog is complete and unambiguous', () => {
|
||||
// Keep every future fixture producer constrained to the same product catalog as Admin and Firstleg.
|
||||
const catalog = Object.freeze({
|
||||
'20GP': Object.freeze({ capacityCbm: 33, maxWeightKg: 28000 }),
|
||||
'40GP': Object.freeze({ capacityCbm: 67, maxWeightKg: 28000 }),
|
||||
'40HC': Object.freeze({ capacityCbm: 76, maxWeightKg: 28000 }),
|
||||
'40NR': Object.freeze({ capacityCbm: 67, maxWeightKg: 28000 }),
|
||||
'45HC': Object.freeze({ capacityCbm: 86, maxWeightKg: 28000 }),
|
||||
})
|
||||
|
||||
assert.deepEqual(Object.keys(catalog), ['20GP', '40GP', '40HC', '40NR', '45HC'])
|
||||
assert.equal(new Set(Object.keys(catalog)).size, 5)
|
||||
})
|
||||
@@ -97,11 +97,12 @@ async function readInbound(context, fixture) {
|
||||
|
||||
async function createConfirmedContainer(context, source, label) {
|
||||
const value = suffix(context, label)
|
||||
const containerNo = `MAN-${value}`.slice(0, 64)
|
||||
const summary = source.detail.summary
|
||||
const draft = {
|
||||
customerId: source.tenantId, customerName: summary.customerName || 'IT 测试客户',
|
||||
channelCode: summary.channelCode, warehouseId: source.warehouseId,
|
||||
warehouseName: summary.warehouseName, billOfLadingNo: `BL-${value}`.slice(0, 128),
|
||||
billOfLadingNo: `BL-${value}`.slice(0, 128),
|
||||
shippingCompanyId: null, shippingCompanyName: null,
|
||||
bookingAgentId: null, bookingAgentName: null, departurePortCode: 'CNSZX',
|
||||
vesselName: `人工船-${value}`.slice(0, 128), voyageNo: `V-${value}`.slice(0, 64),
|
||||
@@ -112,12 +113,12 @@ async function createConfirmedContainer(context, source, label) {
|
||||
parties: [{ partyType: 'SHIPPER', partyName: `发货方-${value}` },
|
||||
{ partyType: 'CONSIGNEE', partyName: `收货方-${value}` },
|
||||
{ partyType: 'NOTIFY_PARTY', partyName: `通知方-${value}` }],
|
||||
containerRequirements: [{ containerType: '40HQ', containerKind: 'FCL', capacityCbm: 68,
|
||||
maxWeightKg: 28000, quantity: 1 }],
|
||||
containers: [{ containerNo, containerType: '40HC', containerKind: 'FCL', capacityCbm: 76,
|
||||
maxWeightKg: 28000 }],
|
||||
}
|
||||
const simulationResponse = await context.request('POST', CONTAINERS, {
|
||||
channelCode: summary.channelCode, warehouseId: source.warehouseId, containerType: '40HQ',
|
||||
capacityCbm: 68, maxWeightKg: 28000,
|
||||
channelCode: summary.channelCode, warehouseId: source.warehouseId, containerType: '40HC',
|
||||
capacityCbm: 76, maxWeightKg: 28000,
|
||||
})
|
||||
const simulation = context.apiSuccess(MODULE, `${label} 新增内部模拟柜`, simulationResponse, '', OPTIONS)
|
||||
const containerId = id(simulation?.id)
|
||||
@@ -130,10 +131,10 @@ async function createConfirmedContainer(context, source, label) {
|
||||
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 requirement = confirmed?.containers?.find?.((item) => item.containerNo === containerNo)
|
||||
if (!requirement?.containerBookingId) throw new Error(`${label} 确认订舱未返回真实柜身份`)
|
||||
const linkedResponse = await context.request('POST', `${CONTAINERS}/${containerId}/ocean-booking-link`, {
|
||||
oceanBookingId: bookingId, requirementId: id(requirement.id), expectedVersion: Number(simulation.version),
|
||||
realContainerBookingId: id(requirement.containerBookingId), expectedVersion: Number(simulation.version),
|
||||
})
|
||||
const linked = context.apiSuccess(MODULE, `${label} 关联模拟柜与真实订舱`, linkedResponse, '', OPTIONS)
|
||||
if (id(linked?.id) !== containerId || linked?.cabinetNo !== simulation.cabinetNo) {
|
||||
|
||||
@@ -22,7 +22,7 @@ function oceanPayload(context, template, index) {
|
||||
return { expectedVersion: 0, data: {
|
||||
customerId: source.customerId, customerName: source.customerName,
|
||||
channelCode: source.channelCode,
|
||||
warehouseId: source.warehouseId, warehouseName: source.warehouseName,
|
||||
warehouseId: source.warehouseId,
|
||||
billOfLadingNo: `BL-${suffix(context, index)}`, vesselName: `人工轮-${suffix(context, index)}`,
|
||||
voyageNo: `V${suffix(context, index)}`,
|
||||
shippingCompanyId: source.shippingCompanyId,
|
||||
@@ -36,8 +36,9 @@ function oceanPayload(context, template, index) {
|
||||
remark: `${context.marker} 人工列表状态 ${index}`,
|
||||
parties: template.parties.map(({ partyType, partyName, address, contactName, contactPhone }) =>
|
||||
({ partyType, partyName, address, contactName, contactPhone })),
|
||||
containerRequirements: template.containerRequirements.map(({ containerType, containerKind, capacityCbm, maxWeightKg, quantity }) =>
|
||||
({ containerType, containerKind, capacityCbm, maxWeightKg, quantity })),
|
||||
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 })),
|
||||
} }
|
||||
}
|
||||
|
||||
@@ -138,8 +139,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?.containerRequirements?.length) {
|
||||
throw new Error('订舱模板缺少渠道、仓库、参与方或箱型需求')
|
||||
if (!template?.oceanBooking || !template?.parties?.length || !template?.containers?.length) {
|
||||
throw new Error('订舱模板缺少渠道、仓库、参与方或真实柜明细')
|
||||
}
|
||||
matrix.pages.oceanBookings = { requiredStatuses: OCEAN_LIST_STATUSES, fixtures: {} }
|
||||
for (const [index, status] of OCEAN_STATUSES.entries()) {
|
||||
|
||||
@@ -31,23 +31,25 @@ 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 data omits server-owned channelName and includes offline-confirmed transport snapshots', () => {
|
||||
test('shared booking data omits server-owned names and registers unique real containers', () => {
|
||||
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', 'warehouseId', 'warehouseName']) {
|
||||
for (const key of ['customerId', 'customerName', 'channelCode', 'warehouseId']) {
|
||||
assert.ok(draft[key], `${key} is required`)
|
||||
}
|
||||
assert.equal(draft.channelName, undefined)
|
||||
assert.equal(draft.warehouseName, undefined)
|
||||
assert.equal(draft.shippingCompanyId, null)
|
||||
assert.equal(draft.shippingCompanyName, null)
|
||||
assert.equal(draft.bookingAgentId, null)
|
||||
assert.equal(draft.bookingAgentName, null)
|
||||
assert.equal(draft.serviceTerm, 'CY-CY')
|
||||
assert.equal(draft.freightTerm, 'NINGBO')
|
||||
assert.equal(draft.containerRequirements[0].capacityCbm, 68)
|
||||
assert.equal(draft.containerRequirements[0].maxWeightKg, 28000)
|
||||
assert.deepEqual(draft.containers, [{ containerNo: 'TST-3-01', containerType: '40HC',
|
||||
containerKind: 'FCL', capacityCbm: 76, maxWeightKg: 28000 }])
|
||||
assert.equal(Object.hasOwn(draft, 'containerRequirements'), false)
|
||||
assert.equal(draft.billOfLadingNo, 'BL-3')
|
||||
assert.equal(draft.vesselName, '验收船-3')
|
||||
assert.equal(draft.voyageNo, 'V-3')
|
||||
@@ -78,8 +80,22 @@ test('shared booking draft can omit customer identity without inventing placehol
|
||||
assert.equal(Object.hasOwn(draft, 'customerName'), false)
|
||||
assert.equal(draft.channelCode, 'SEA-US')
|
||||
assert.equal(draft.warehouseId, '41')
|
||||
assert.equal(draft.containerRequirements[0].capacityCbm, 68)
|
||||
assert.equal(draft.containerRequirements[0].maxWeightKg, 28000)
|
||||
assert.equal(draft.containers[0].containerType, '40HC')
|
||||
assert.equal(draft.containers[0].capacityCbm, 76)
|
||||
assert.equal(draft.containers[0].maxWeightKg, 28000)
|
||||
})
|
||||
|
||||
test('shared booking builder accepts explicit isolated multi-container rows without quantity', () => {
|
||||
const containers = [
|
||||
{ containerNo: 'TST-MULTI-01', containerType: '40HC', containerKind: 'FCL', capacityCbm: 76, maxWeightKg: 28000 },
|
||||
{ containerNo: 'TST-MULTI-02', containerType: '20GP', containerKind: 'FCL', capacityCbm: 33, maxWeightKg: 28000 },
|
||||
]
|
||||
const draft = oceanBookingDraft({ marker: 'ADMIN-RUN-MULTI' }, 'multi', {
|
||||
customerId: '21', customerName: 'IT 测试客户', channelCode: 'SEA-US', channelName: '美西海运',
|
||||
warehouseId: '41', warehouseName: '深圳仓',
|
||||
}, { containers })
|
||||
assert.deepEqual(draft.containers, containers)
|
||||
assert.equal(draft.containers.some((item) => Object.hasOwn(item, 'quantity')), false)
|
||||
})
|
||||
|
||||
test('platform channel producer queries the selected origin warehouse and publishes current-marker resources', async () => {
|
||||
@@ -106,18 +122,22 @@ test('platform channel producer queries the selected origin warehouse and publis
|
||||
|
||||
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, '订舱渠道与仓库快照完整'"],
|
||||
['manual-state-loading-pda.mjs', 'const draft = {', 'const createResponse', false],
|
||||
['manual-state-ocean-trucking.mjs', 'function oceanPayload', 'async function createOcean', false],
|
||||
['phase3-loading.mjs', 'const bookingDraft = {', 'const bookingCreate', false],
|
||||
['phase3-order-splitting.mjs', 'function bookingPayload', 'async function loadOneFragment', true],
|
||||
['phase3-container-loading.mjs', 'const bookingDraft = {', "context.assert(MODULE, '订舱渠道与仓库快照完整'", false],
|
||||
]
|
||||
for (const [name, start, end] of regions) {
|
||||
for (const [name, start, end, usesSharedBuilder] 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)
|
||||
for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) {
|
||||
assert.match(region, new RegExp(`${field}\\s*:`), `${name} must register ${field}`)
|
||||
if (usesSharedBuilder) {
|
||||
assert.match(region, /oceanBookingDraft\(/, `${name} must use the shared formal booking builder`)
|
||||
} else {
|
||||
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')
|
||||
@@ -140,5 +160,5 @@ test('all independent ocean booking producers use the shared master-data contrac
|
||||
}
|
||||
const container = readFileSync(new URL('phase3-container-loading.mjs', import.meta.url), 'utf8')
|
||||
for (const snippet of ['channelCode', 'publishPlatformLogisticsChannels', 'warehouseId', 'warehouseName',
|
||||
'capacityCbm: 100', 'maxWeightKg: 100000']) assert.match(container, new RegExp(snippet), snippet)
|
||||
"containerType: '40HC'", 'capacityCbm: 76', 'maxWeightKg: 28000']) assert.match(container, new RegExp(snippet), snippet)
|
||||
})
|
||||
|
||||
@@ -53,7 +53,12 @@ export async function resolveOceanBookingMaster(context, module) {
|
||||
}
|
||||
|
||||
export function oceanBookingDraft(context, suffix, master, labels = {}) {
|
||||
const { channelName: _readOnlyChannelName, ...writableMaster } = master
|
||||
const { channelName: _readOnlyChannelName, warehouseName: _readOnlyWarehouseName, ...writableMaster } = master
|
||||
const normalizedSuffix = text(suffix).replace(/[^A-Za-z0-9]/g, '').slice(-32) || 'BOOKING'
|
||||
const containers = labels.containers || [{
|
||||
containerNo: `TST-${normalizedSuffix}-01`.slice(0, 64),
|
||||
containerType: '40HC', containerKind: 'FCL', capacityCbm: 76, maxWeightKg: 28000,
|
||||
}]
|
||||
return {
|
||||
...writableMaster,
|
||||
billOfLadingNo: labels.billOfLadingNo || `BL-${suffix}`,
|
||||
@@ -70,7 +75,7 @@ export function oceanBookingDraft(context, suffix, master, labels = {}) {
|
||||
parties: [{ partyType: 'SHIPPER', partyName: `发货方-${suffix}` },
|
||||
{ partyType: 'CONSIGNEE', partyName: `收货方-${suffix}` },
|
||||
{ partyType: 'NOTIFY_PARTY', partyName: `通知方-${suffix}` }],
|
||||
containerRequirements: [{ containerType: '40HQ', containerKind: 'FCL', capacityCbm: 68, maxWeightKg: 28000, quantity: 1 }],
|
||||
containers,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,15 @@ async function request(context, method, path, body, headers = {}) {
|
||||
} catch (error) { return { method, path, http: 0, body: null, raw: String(error), requestId: null, traceId: null } }
|
||||
}
|
||||
function data(r) { return String(r?.body?.code) === '200' ? r.body.data : null }
|
||||
async function createBooking(context, suffix, key, master) { return request(context, 'POST', BASE, { expectedVersion: 0, data: oceanBookingDraft(context, suffix, master) }, { 'Idempotency-Key': key }) }
|
||||
async function createBooking(context, suffix, key, master) {
|
||||
const containers = [
|
||||
{ containerNo: `TST-${suffix}-01`.slice(0, 64), containerType: '40HC', containerKind: 'FCL', capacityCbm: 76, maxWeightKg: 28000 },
|
||||
{ containerNo: `TST-${suffix}-02`.slice(0, 64), containerType: '20GP', containerKind: 'FCL', capacityCbm: 33, maxWeightKg: 28000 },
|
||||
]
|
||||
return request(context, 'POST', BASE,
|
||||
{ expectedVersion: 0, data: oceanBookingDraft(context, suffix, master, { containers }) },
|
||||
{ 'Idempotency-Key': key })
|
||||
}
|
||||
function step(context, name, r, pass, evidence, options = OPTIONS) { context.addStep(MODULE, name, r, pass, evidence, { ...options, deploymentBlocker: options.deploymentBlocker && !pass }) }
|
||||
|
||||
async function createConfirmedUnassignedBooking(context, suffix, master) {
|
||||
@@ -37,10 +45,10 @@ async function createConfirmedUnassignedBooking(context, suffix, master) {
|
||||
const detailResponse = await request(context, 'GET', path)
|
||||
const detail = context.apiSuccess(MODULE, '回查未指定客户订舱持久化结果', detailResponse, '', OPTIONS)
|
||||
const bookingNo = id(detail?.oceanBooking?.bookingNo || created?.oceanBooking?.bookingNo)
|
||||
context.assert(MODULE, '未指定客户订舱保持空客户且 CONFIRMED 不自动生成内部柜', detailResponse,
|
||||
context.assert(MODULE, '未指定客户订舱保持空客户且原子生成一条真实柜', detailResponse,
|
||||
detail?.oceanBooking?.status === 'CONFIRMED' && detail?.oceanBooking?.customerId == null
|
||||
&& detail?.oceanBooking?.customerName == null && detail?.containerAllocations?.length === 0 && Boolean(bookingNo),
|
||||
`status=${detail?.oceanBooking?.status || 'missing'}, customerId=${id(detail?.oceanBooking?.customerId) || 'null'}, bookingNo=${bookingNo || 'missing'}, allocations=${detail?.containerAllocations?.length ?? 'n/a'}`, OPTIONS)
|
||||
&& detail?.oceanBooking?.customerName == null && detail?.containers?.length === 1 && Boolean(bookingNo),
|
||||
`status=${detail?.oceanBooking?.status || 'missing'}, customerId=${id(detail?.oceanBooking?.customerId) || 'null'}, bookingNo=${bookingNo || 'missing'}, containers=${detail?.containers?.length ?? 'n/a'}`, OPTIONS)
|
||||
|
||||
const listResponse = await request(context, 'GET', `${BASE}?page=1&size=20&keyword=${encodeURIComponent(bookingNo)}`)
|
||||
const page = context.apiSuccess(MODULE, '按真实订舱号查询未指定客户订舱', listResponse, `bookingNo=${bookingNo}`, OPTIONS)
|
||||
@@ -48,7 +56,8 @@ async function createConfirmedUnassignedBooking(context, suffix, master) {
|
||||
page?.records?.some?.((item) => id(item.id) === bookingId && item.customerId == null && item.customerName == null),
|
||||
`bookingId=${bookingId}, bookingNo=${bookingNo}, records=${page?.records?.length ?? 'n/a'}`, OPTIONS)
|
||||
const resource = { id: bookingId, marker: context.marker, customerId: null, customerName: null,
|
||||
bookingNo, channelCode: draft.channelCode, warehouseId: draft.warehouseId, containerBookingId: null,
|
||||
bookingNo, channelCode: draft.channelCode, warehouseId: draft.warehouseId,
|
||||
containerBookingId: id(detail?.containers?.[0]?.containerBookingId) || null,
|
||||
finalStatus: detail?.oceanBooking?.status || null }
|
||||
context.createdResources.oceanBookingUnassignedCustomer = resource
|
||||
return resource
|
||||
@@ -62,11 +71,11 @@ async function releaseLoadingBooking(context, containerFlow, suffix) {
|
||||
let flow = data(flowResponse)
|
||||
context.apiSuccess(MODULE, '回读实际装货柜次所属订舱', flowResponse,
|
||||
`oceanBookingId=${flowOceanBookingId}`, OPTIONS)
|
||||
const allocationMatches = flow?.containerAllocations?.some?.((item) =>
|
||||
const allocationMatches = flow?.containers?.some?.((item) =>
|
||||
id(item?.containerBookingId) === id(containerFlow.containerBookingId))
|
||||
context.assert(MODULE, '实际装货柜次只绑定其所属订舱', flowResponse,
|
||||
allocationMatches && id(flow?.oceanBooking?.customerId) === id(containerFlow?.tenantId),
|
||||
`oceanBookingId=${flowOceanBookingId}, containerBookingId=${id(containerFlow.containerBookingId)}, allocations=${flow?.containerAllocations?.length ?? 'n/a'}`, OPTIONS)
|
||||
`oceanBookingId=${flowOceanBookingId}, containerBookingId=${id(containerFlow.containerBookingId)}, containers=${flow?.containers?.length ?? 'n/a'}`, OPTIONS)
|
||||
if (allocationMatches && flow?.oceanBooking?.status === 'CONFIRMED') {
|
||||
const transportFields = ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']
|
||||
const frozenTransport = Object.fromEntries(transportFields.map((field) => [field, flow?.oceanBooking?.[field]]))
|
||||
@@ -84,7 +93,7 @@ async function releaseLoadingBooking(context, containerFlow, suffix) {
|
||||
}
|
||||
context.assert(MODULE, '实际装货柜次订舱已放舱供下游串联', flowResponse,
|
||||
flow?.oceanBooking?.status === 'RELEASED'
|
||||
&& flow?.containerAllocations?.some?.((item) => id(item?.containerBookingId) === id(containerFlow.containerBookingId)),
|
||||
&& flow?.containers?.some?.((item) => id(item?.containerBookingId) === id(containerFlow.containerBookingId)),
|
||||
`status=${flow?.oceanBooking?.status || 'missing'}, containerBookingId=${id(containerFlow.containerBookingId)}`, OPTIONS)
|
||||
return flow?.oceanBooking?.status === 'RELEASED' && allocationMatches ? {
|
||||
id: flowOceanBookingId, customerId: id(flow.oceanBooking.customerId), marker: context.marker,
|
||||
@@ -114,6 +123,24 @@ export async function run(context) {
|
||||
}
|
||||
const page = context.apiSuccess(MODULE, '订舱列表分页查询', list, '', OPTIONS)
|
||||
context.assert(MODULE, '订舱列表分页结构正确', list, Array.isArray(page?.records) && Number.isFinite(Number(page?.total)), `records=${page?.records?.length ?? 'n/a'}`, OPTIONS)
|
||||
const duplicateNo = `TST-${suffix}-DUP`.slice(0, 64)
|
||||
const duplicateDraft = oceanBookingDraft(context, `${suffix}D`, master, { containers: [
|
||||
{ containerNo: duplicateNo, containerType: '40HC', containerKind: 'FCL', capacityCbm: 76, maxWeightKg: 28000 },
|
||||
{ containerNo: duplicateNo, containerType: '40HC', containerKind: 'FCL', capacityCbm: 76, maxWeightKg: 28000 },
|
||||
] })
|
||||
const duplicateCreate = await request(context, 'POST', BASE, { expectedVersion: 0, data: duplicateDraft },
|
||||
{ 'Idempotency-Key': `${context.marker}-ocean-duplicate-in-request`.slice(0, 128) })
|
||||
step(context, '同一订舱请求重复真实柜号原子拒绝', duplicateCreate, failure(duplicateCreate),
|
||||
`containerNo=${duplicateNo}, HTTP=${duplicateCreate.http}, code=${codeOf(duplicateCreate)}`)
|
||||
const duplicateListResponse = await request(context, 'GET', `${BASE}?page=1&size=5`)
|
||||
const duplicatePage = context.apiSuccess(MODULE, '回查请求内重复柜号失败后的订舱列表', duplicateListResponse, '', OPTIONS)
|
||||
const duplicateContainerResponse = await request(context, 'GET',
|
||||
`/api/platform/v1/containers?page=1&size=20&keyword=${encodeURIComponent(duplicateNo)}`)
|
||||
const duplicateContainers = context.apiSuccess(MODULE, '回查请求内重复柜号失败后的货柜列表', duplicateContainerResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '请求内重复柜号失败后订舱和货柜零写入', duplicateContainerResponse,
|
||||
Number(duplicatePage?.total) === Number(page?.total)
|
||||
&& Array.isArray(duplicateContainers?.records) && duplicateContainers.records.length === 0,
|
||||
`bookingTotalBefore=${page?.total ?? 'n/a'}, bookingTotalAfter=${duplicatePage?.total ?? 'n/a'}, containers=${duplicateContainers?.records?.length ?? 'n/a'}`, OPTIONS)
|
||||
const statuses = ['CONFIRMED', 'CHANGE_PENDING', 'RELEASED', 'CANCELLED']
|
||||
for (const status of statuses) { const r = await request(context, 'GET', `${BASE}?page=1&size=5&status=${status}`); const d = data(r); step(context, `订舱 ${status} 状态筛选`, r, r.http === 200 && codeOf(r) === '200' && Array.isArray(d?.records) && d.records.every((x) => x.status === status), `HTTP=${r.http}, code=${codeOf(r)}, records=${d?.records?.length ?? 'n/a'}`) }
|
||||
for (const status of ['DRAFT', 'SUBMITTED']) { const r = await request(context, 'GET', `${BASE}?page=1&size=5&status=${status}`); step(context, `正式订舱列表拒绝 ${status} 状态`, r, failure(r), `HTTP=${r.http}, code=${codeOf(r)}`) }
|
||||
@@ -126,18 +153,39 @@ export async function run(context) {
|
||||
context.apiSuccess(MODULE, '创建隔离海运订舱', createResponse, `key=${key}`, OPTIONS)
|
||||
context.assert(MODULE, '线下已确认订舱 DTO、运输快照和关联字段完整', createResponse,
|
||||
Boolean(bookingId) && detail?.oceanBooking?.status === 'CONFIRMED'
|
||||
&& detail?.parties?.length === 3 && detail?.containerRequirements?.length === 1
|
||||
&& detail?.parties?.length === 3 && detail?.containers?.length === 2
|
||||
&& ['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)
|
||||
`bookingId=${bookingId || 'missing'}, status=${detail?.oceanBooking?.status || 'missing'}, parties=${detail?.parties?.length ?? 'n/a'}, containers=${detail?.containers?.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)
|
||||
const expectedContainerNos = [`TST-${suffix}-01`.slice(0, 64), `TST-${suffix}-02`.slice(0, 64)]
|
||||
context.assert(MODULE, '一单两柜逐柜原子生成独立需求分配和 ACTIVE 操作身份', createResponse,
|
||||
detail?.containers?.length === 2 && expectedContainerNos.every((containerNo) => {
|
||||
const item = detail.containers.find((value) => value.containerNo === containerNo)
|
||||
return Boolean(item?.containerBookingId && item?.cabinetNo && item?.requirementId && item?.allocationId)
|
||||
&& id(item?.warehouseId) === id(master.warehouseId) && item?.channelCode === master.channelCode
|
||||
&& id(item?.customerId) === id(master.customerId)
|
||||
}),
|
||||
`expected=${expectedContainerNos.join('|')}, actual=${detail?.containers?.map?.((item) => item.containerNo).join('|') || 'none'}`, OPTIONS)
|
||||
if (!bookingId) return
|
||||
context.createdResources.oceanBooking = { id: bookingId, customerId: master.customerId, marker: context.marker,
|
||||
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 existingContainerNo = detail?.containers?.[0]?.containerNo
|
||||
const globalDuplicateDraft = oceanBookingDraft(context, `${suffix}G`, master, { containers: [{
|
||||
containerNo: existingContainerNo, containerType: detail?.containers?.[0]?.containerType,
|
||||
containerKind: detail?.containers?.[0]?.containerKind, capacityCbm: detail?.containers?.[0]?.capacityCbm,
|
||||
maxWeightKg: detail?.containers?.[0]?.maxWeightKg,
|
||||
}] })
|
||||
const globalDuplicate = await request(context, 'POST', BASE,
|
||||
{ expectedVersion: 0, data: globalDuplicateDraft },
|
||||
{ 'Idempotency-Key': `${context.marker}-ocean-global-duplicate`.slice(0, 128) })
|
||||
step(context, '数据库全局重复真实柜号原子拒绝', globalDuplicate,
|
||||
Boolean(existingContainerNo) && failure(globalDuplicate),
|
||||
`containerNo=${existingContainerNo || 'missing'}, HTTP=${globalDuplicate.http}, code=${codeOf(globalDuplicate)}`)
|
||||
const path = `${BASE}/${bookingId}`
|
||||
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 }
|
||||
@@ -164,13 +212,15 @@ export async function run(context) {
|
||||
context.assert(MODULE, '订舱与本轮柜次客户归属一致', changed,
|
||||
id(containerFlow?.tenantId) === id(changeConfirmed?.oceanBooking?.customerId),
|
||||
`bookingCustomerId=${id(changeConfirmed?.oceanBooking?.customerId) || 'missing'}, containerTenantId=${id(containerFlow?.tenantId) || 'missing'}`, OPTIONS)
|
||||
const requirement = changeConfirmed?.containerRequirements?.find?.((item) => item?.containerType === '40HQ')
|
||||
context.assert(MODULE, '订舱存在匹配本轮柜型的箱型需求', changed,
|
||||
Boolean(requirement?.id), `requirementId=${id(requirement?.id) || 'missing'}`, OPTIONS)
|
||||
if (!requirement?.id || id(containerFlow?.tenantId) !== id(changeConfirmed?.oceanBooking?.customerId)) return
|
||||
context.assert(MODULE, '独立订舱生命周期不占用本轮已装货柜次', changed,
|
||||
changeConfirmed?.containerAllocations?.length === 0 && Number.isInteger(version),
|
||||
`allocations=${changeConfirmed?.containerAllocations?.length ?? 'n/a'}, version=${version}`, OPTIONS)
|
||||
const realContainer = changeConfirmed?.containers?.find?.((item) => item?.containerType === '40HC')
|
||||
context.assert(MODULE, '独立订舱存在完整匹配柜型的真实柜身份', changed,
|
||||
Boolean(realContainer?.containerBookingId && realContainer?.containerNo && realContainer?.requirementId && realContainer?.allocationId),
|
||||
`containerBookingId=${id(realContainer?.containerBookingId) || 'missing'}`, OPTIONS)
|
||||
if (!realContainer?.containerBookingId || id(containerFlow?.tenantId) !== id(changeConfirmed?.oceanBooking?.customerId)) return
|
||||
context.assert(MODULE, '独立订舱真实柜不占用本轮已装货柜次身份', changed,
|
||||
!changeConfirmed?.containers?.some?.((item) => id(item?.containerBookingId) === id(containerFlow?.containerBookingId))
|
||||
&& Number.isInteger(version),
|
||||
`containers=${changeConfirmed?.containers?.length ?? 'n/a'}, version=${version}`, OPTIONS)
|
||||
}
|
||||
const released = await request(context, 'POST', `${path}/release`, {
|
||||
expectedVersion: version, ...frozenTransport, reason: context.marker,
|
||||
@@ -182,8 +232,8 @@ export async function run(context) {
|
||||
context.apiSuccess(MODULE, '回查 RELEASED 订舱详情', final, '', OPTIONS)
|
||||
context.assert(MODULE, '独立订舱状态历史、审计操作者和终态完整', final,
|
||||
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)
|
||||
&& fd?.containers?.length === 2,
|
||||
`status=${fd?.oceanBooking?.status || 'missing'}, histories=${fd?.histories?.length ?? 'n/a'}, containers=${fd?.containers?.length ?? 'n/a'}`, OPTIONS)
|
||||
let downstreamBooking = {
|
||||
id: bookingId, customerId: id(fd?.oceanBooking?.customerId), marker: context.marker,
|
||||
containerBookingId: allocatedContainerBookingId || null,
|
||||
|
||||
@@ -42,7 +42,6 @@ async function request(context, method, path, body, headers = {}) {
|
||||
function oceanDraft(context, suffix, master) {
|
||||
return {
|
||||
...oceanBookingDraft(context, suffix, master),
|
||||
shippingCompanyName: 'ZIM', bookingAgentName: `船司查询代理-${suffix}`,
|
||||
departurePortCode: 'CNSZX',
|
||||
departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
|
||||
placeOfDelivery: 'LAX', serviceTerm: 'CY-CY', freightTerm: 'NINGBO',
|
||||
@@ -52,7 +51,6 @@ function oceanDraft(context, suffix, master) {
|
||||
{ partyType: 'CONSIGNEE', partyName: `收货方-${suffix}` },
|
||||
{ partyType: 'NOTIFY_PARTY', partyName: `通知方-${suffix}` },
|
||||
],
|
||||
containerRequirements: [{ containerType: '40HQ', containerKind: 'FCL', capacityCbm: 68, maxWeightKg: 28000, quantity: 1 }],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,10 +112,12 @@ export async function run(context) {
|
||||
OPTIONS)
|
||||
if (!selectedChannel) return
|
||||
const channelName = selectedChannel.channelName
|
||||
const primaryRequirement = { containerType: '40HQ', containerKind: 'FCL', capacityCbm: 100, maxWeightKg: 100000, quantity: 1 }
|
||||
const primaryRequirement = { containerType: '40HC', containerKind: 'FCL', capacityCbm: 76, maxWeightKg: 28000 }
|
||||
const realContainerNo = `TST-${suffix}-01`.slice(0, 64)
|
||||
const wrongContainerNo = `TST-${suffix}-02`.slice(0, 64)
|
||||
const bookingDraft = {
|
||||
customerId: tenantId, customerName: inbound?.summary?.customerName || `验收客户-${tenantId}`,
|
||||
channelCode, warehouseId, warehouseName,
|
||||
channelCode, warehouseId,
|
||||
billOfLadingNo: `BL-${suffix}`.slice(0, 128),
|
||||
shippingCompanyId: null, shippingCompanyName: null,
|
||||
bookingAgentId: null, bookingAgentName: null,
|
||||
@@ -127,8 +129,10 @@ export async function run(context) {
|
||||
parties: [{ partyType: 'SHIPPER', partyName: `发货方-${suffix}` },
|
||||
{ partyType: 'CONSIGNEE', partyName: `收货方-${suffix}` },
|
||||
{ partyType: 'NOTIFY_PARTY', partyName: `通知方-${suffix}` }],
|
||||
containerRequirements: [primaryRequirement,
|
||||
{ containerType: '20GP', containerKind: 'FCL', capacityCbm: 28, maxWeightKg: 22000, quantity: 1 }],
|
||||
containers: [
|
||||
{ containerNo: realContainerNo, ...primaryRequirement },
|
||||
{ containerNo: wrongContainerNo, containerType: '20GP', containerKind: 'FCL', capacityCbm: 33, maxWeightKg: 28000 },
|
||||
],
|
||||
}
|
||||
context.assert(MODULE, '订舱渠道与仓库快照完整', inboundResponse, Boolean(channelCode && channelName && warehouseName),
|
||||
`channelCode=${channelCode || 'missing'}, warehouseId=${warehouseId}, warehouseName=${warehouseName || 'missing'}`, OPTIONS)
|
||||
@@ -324,30 +328,16 @@ export async function run(context) {
|
||||
`changed=${rejectedDiff.join('|') || 'none'}; sameKey=${responseEvidence(pushSameKeyDifferentBody)}; newKey=${responseEvidence(pushNewKeyDuplicate)}; task=${responseEvidence(rejectedTaskResponse)}`, OPTIONS)
|
||||
if (rejectedDiff.length !== 0 || codeOf(rejectedTaskResponse) !== 'A0404') return
|
||||
|
||||
const realContainerNo = `TST-${suffix}`.slice(0, 64)
|
||||
const transportResponse = await request(context, 'PATCH',
|
||||
`${FIRST_LEG}/loading-plans/${planId}/transport?warehouseId=${encodeURIComponent(warehouseId)}`, {
|
||||
containerNo: realContainerNo, voyageNo: `V-${suffix}`.slice(0, 64),
|
||||
expectedVersion: Number(postPushPlan.version), operationReason: context.marker,
|
||||
}, { 'Idempotency-Key': `${context.marker}-preload-bind-container-no`.slice(0, 128) })
|
||||
const transported = context.apiSuccess(MODULE, '预装 EDITING 清单绑定本轮真实柜号', transportResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '预装补号保持柜次和计划身份稳定', transportResponse,
|
||||
transported?.summary?.containerNo === realContainerNo
|
||||
&& id(transported?.summary?.planId) === planId
|
||||
&& id(transported?.summary?.containerBookingId) === containerId,
|
||||
`containerNo=${transported?.summary?.containerNo || 'missing'}, containerBookingId=${id(transported?.summary?.containerBookingId) || 'missing'}, loadingPlanId=${id(transported?.summary?.planId) || 'missing'}`, OPTIONS)
|
||||
if (transported?.summary?.containerNo !== realContainerNo) return
|
||||
|
||||
const unlinkedTaskResponse = await context.request('GET',
|
||||
`${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const unlinkedTaskAbsent = unlinkedTaskResponse.http === 200 && codeOf(unlinkedTaskResponse) === 'A0404'
|
||||
context.addStep(MODULE, '已补号但未关联订舱不生成 PDA 任务', unlinkedTaskResponse,
|
||||
context.addStep(MODULE, '无号且未关联订舱不生成 PDA 任务', unlinkedTaskResponse,
|
||||
unlinkedTaskAbsent, responseEvidence(unlinkedTaskResponse), OPTIONS)
|
||||
if (!unlinkedTaskAbsent) return
|
||||
|
||||
const transportedContainerResponse = await context.request('GET', `${CONTAINERS}/${containerId}`)
|
||||
const transportedContainer = context.apiSuccess(MODULE, '预装补号后读取模拟柜最新版本', transportedContainerResponse, '', OPTIONS)
|
||||
if (!Number.isInteger(Number(transportedContainer?.version))) return
|
||||
const simulationBeforeLinkResponse = await context.request('GET', `${CONTAINERS}/${containerId}`)
|
||||
const simulationBeforeLink = context.apiSuccess(MODULE, '预装确认前读取模拟柜最新版本', simulationBeforeLinkResponse, '', OPTIONS)
|
||||
if (!Number.isInteger(Number(simulationBeforeLink?.version))) return
|
||||
|
||||
const bookingCreate = await request(context, 'POST', '/api/platform/v1/ocean-bookings',
|
||||
{ expectedVersion: 0, data: bookingDraft }, { 'Idempotency-Key': `${context.marker}-loading-booking-create` })
|
||||
@@ -363,8 +353,8 @@ export async function run(context) {
|
||||
&& String(frozenTransport[field]) === String(bookingDraft[field]))
|
||||
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)
|
||||
&& Array.isArray(confirmedBooking?.containers) && confirmedBooking.containers.length === 2,
|
||||
`status=${confirmedBooking?.oceanBooking?.status || 'missing'}, containers=${confirmedBooking?.containers?.length ?? 'n/a'}`, OPTIONS)
|
||||
context.assert(MODULE, '已确认订舱回显完整且一致的冻结运输快照', bookingCreate,
|
||||
frozenTransportMatches,
|
||||
transportFields.map((field) => `${field}=${frozenTransport[field] || 'missing'}`).join(', '), OPTIONS)
|
||||
@@ -376,50 +366,18 @@ export async function run(context) {
|
||||
records(confirmedList).some((item) => id(item.id) === bookingId && item.status === 'CONFIRMED'),
|
||||
`bookingId=${bookingId}, records=${records(confirmedList).length}`, OPTIONS)
|
||||
|
||||
const requirement = confirmedBooking?.containerRequirements?.find?.((item) => item.containerType === primaryRequirement.containerType)
|
||||
const wrongTypeRequirement = confirmedBooking?.containerRequirements?.find?.((item) => item.containerType !== primaryRequirement.containerType)
|
||||
if (!requirement?.id || !wrongTypeRequirement?.id) return
|
||||
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`, {
|
||||
oceanBookingId: bookingId, requirementId: id(wrongTypeRequirement.id), expectedVersion: Number(transportedContainer.version),
|
||||
realContainerBookingId: id(wrongTypeRequirement.containerBookingId), expectedVersion: Number(simulationBeforeLink.version),
|
||||
})
|
||||
context.addStep(MODULE, '箱型不匹配拒绝关联真实订舱柜', wrongTypeLinkResponse,
|
||||
failure(wrongTypeLinkResponse), `HTTP=${wrongTypeLinkResponse.http}, code=${codeOf(wrongTypeLinkResponse)}`, OPTIONS)
|
||||
|
||||
const emptySimulationResponse = await request(context, 'POST', CONTAINERS, {
|
||||
channelCode, warehouseId, containerType: primaryRequirement.containerType,
|
||||
capacityCbm: primaryRequirement.capacityCbm, maxWeightKg: primaryRequirement.maxWeightKg,
|
||||
})
|
||||
let emptySimulation = context.apiSuccess(MODULE, '新增空模拟柜验证解绑换绑', emptySimulationResponse, '', OPTIONS)
|
||||
const emptyContainerId = id(emptySimulation?.id)
|
||||
if (!emptyContainerId) return
|
||||
const emptyLink = await request(context, 'POST', `${CONTAINERS}/${emptyContainerId}/ocean-booking-link`, {
|
||||
oceanBookingId: bookingId, requirementId: id(requirement.id), expectedVersion: Number(emptySimulation.version),
|
||||
})
|
||||
emptySimulation = context.apiSuccess(MODULE, '空模拟柜首次关联真实订舱', emptyLink,
|
||||
responseEvidence(emptyLink), OPTIONS)
|
||||
if (!emptySimulation) return
|
||||
const emptyUnlink = await request(context, 'DELETE', `${CONTAINERS}/${emptyContainerId}/ocean-booking-link`, {
|
||||
expectedVersion: Number(emptySimulation.version), reason: context.marker,
|
||||
})
|
||||
emptySimulation = context.apiSuccess(MODULE, '空模拟柜允许解除关联', emptyUnlink,
|
||||
responseEvidence(emptyUnlink), OPTIONS)
|
||||
if (!emptySimulation) return
|
||||
const emptyRelink = await request(context, 'POST', `${CONTAINERS}/${emptyContainerId}/ocean-booking-link`, {
|
||||
oceanBookingId: bookingId, requirementId: id(requirement.id), expectedVersion: Number(emptySimulation.version),
|
||||
})
|
||||
emptySimulation = context.apiSuccess(MODULE, '空模拟柜解绑后允许重新关联', emptyRelink,
|
||||
responseEvidence(emptyRelink), OPTIONS)
|
||||
if (!emptySimulation) return
|
||||
const emptyFinalUnlink = await request(context, 'DELETE', `${CONTAINERS}/${emptyContainerId}/ocean-booking-link`, {
|
||||
expectedVersion: Number(emptySimulation.version), reason: `${context.marker}-release-capacity`,
|
||||
})
|
||||
const emptyReleased = context.apiSuccess(MODULE, '空模拟柜最终解绑释放真实订舱容量', emptyFinalUnlink,
|
||||
responseEvidence(emptyFinalUnlink), OPTIONS)
|
||||
if (!emptyReleased) 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`, {
|
||||
oceanBookingId: bookingId, requirementId: id(requirement.id), expectedVersion: Number(transportedContainer.version),
|
||||
realContainerBookingId: id(requirement.containerBookingId), expectedVersion: Number(simulationBeforeLink.version),
|
||||
})
|
||||
const linkedContainer = context.apiSuccess(MODULE, '首次关联真实已确认订舱柜', linkResponse,
|
||||
responseEvidence(linkResponse), OPTIONS)
|
||||
@@ -429,19 +387,38 @@ export async function run(context) {
|
||||
const afterLinkItems = JSON.stringify(afterLinkPlan?.items?.map((item) => ({ id: id(item.id), inboundId: id(item.inboundId), packageCount: Number(item.packageCount) })) || [])
|
||||
context.assert(MODULE, '关联前后内部柜次、计划和排货明细身份稳定', linkResponse,
|
||||
id(linkedContainer?.id) === containerId && linkedContainer?.cabinetNo === simulation.cabinetNo
|
||||
&& id(linkedContainer?.oceanBookingId) === bookingId && id(afterLinkPlan?.id) === planId
|
||||
&& id(linkedContainer?.oceanBookingId) === bookingId && linkedContainer?.containerNo === realContainerNo
|
||||
&& id(afterLinkPlan?.id) === planId
|
||||
&& beforeLinkItems === afterLinkItems,
|
||||
`containerBookingId=${containerId}, cabinetNo=${linkedContainer?.cabinetNo || 'missing'}, loadingPlanId=${id(afterLinkPlan?.id) || 'missing'}`, OPTIONS)
|
||||
|
||||
const activeContainersResponse = await context.request('GET',
|
||||
`${CONTAINERS}?page=1&size=50&keyword=${encodeURIComponent(realContainerNo)}`)
|
||||
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)
|
||||
|
||||
const mergedBookingResponse = await context.request('GET', `/api/platform/v1/ocean-bookings/${bookingId}`)
|
||||
const mergedBooking = context.apiSuccess(MODULE, '合并后回查订舱真实柜当前操作身份', mergedBookingResponse, '', OPTIONS)
|
||||
const mergedContainer = mergedBooking?.containers?.find?.((item) => item.containerNo === realContainerNo)
|
||||
context.assert(MODULE, '订舱真实柜 allocation 已原子改指稳定模拟柜身份', mergedBookingResponse,
|
||||
id(mergedContainer?.containerBookingId) === containerId
|
||||
&& id(mergedContainer?.requirementId) === id(requirement.requirementId)
|
||||
&& id(mergedContainer?.allocationId) === id(requirement.allocationId),
|
||||
`containerBookingId=${id(mergedContainer?.containerBookingId) || 'missing'}, requirementId=${id(mergedContainer?.requirementId) || 'missing'}, allocationId=${id(mergedContainer?.allocationId) || 'missing'}`, OPTIONS)
|
||||
|
||||
const duplicateSimulationResponse = await request(context, 'POST', CONTAINERS, {
|
||||
channelCode, warehouseId, containerType: primaryRequirement.containerType,
|
||||
capacityCbm: primaryRequirement.capacityCbm, maxWeightKg: primaryRequirement.maxWeightKg,
|
||||
})
|
||||
const duplicateSimulation = context.apiSuccess(MODULE, '新增重复容量占用负向模拟柜', duplicateSimulationResponse, '', OPTIONS)
|
||||
const duplicateLinkResponse = await request(context, 'POST', `${CONTAINERS}/${id(duplicateSimulation?.id)}/ocean-booking-link`, {
|
||||
oceanBookingId: bookingId, requirementId: id(requirement.id), expectedVersion: Number(duplicateSimulation?.version),
|
||||
realContainerBookingId: id(requirement.containerBookingId), expectedVersion: Number(duplicateSimulation?.version),
|
||||
})
|
||||
context.addStep(MODULE, '同一真实订舱箱型容量拒绝重复关联', duplicateLinkResponse,
|
||||
context.addStep(MODULE, '已消费 MERGED 真实柜拒绝再次关联', duplicateLinkResponse,
|
||||
failure(duplicateLinkResponse), `HTTP=${duplicateLinkResponse.http}, code=${codeOf(duplicateLinkResponse)}`, OPTIONS)
|
||||
|
||||
const unlinkAfterLoadingResponse = await request(context, 'DELETE', `${CONTAINERS}/${containerId}/ocean-booking-link`, {
|
||||
@@ -464,13 +441,14 @@ export async function run(context) {
|
||||
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)
|
||||
&& releasedBooking?.containers?.some?.((item) => id(item.containerBookingId) === containerId
|
||||
&& item.containerNo === realContainerNo)
|
||||
&& transportFields.every((field) => String(releasedBooking?.oceanBooking?.[field]) === String(frozenTransport[field])),
|
||||
`status=${releasedBooking?.oceanBooking?.status || 'missing'}, containerBookingId=${containerId}, transportFrozen=${transportFields.every((field) => String(releasedBooking?.oceanBooking?.[field]) === String(frozenTransport[field]))}`, OPTIONS)
|
||||
if (releasedBooking?.oceanBooking?.status !== 'RELEASED') return
|
||||
|
||||
const preloadPageResponse = await context.request('GET',
|
||||
`${FIRST_LEG}/loading-plans?warehouseId=${encodeURIComponent(warehouseId)}&keyword=${encodeURIComponent(containerNo)}&page=1&size=20`)
|
||||
`${FIRST_LEG}/loading-plans?warehouseId=${encodeURIComponent(warehouseId)}&keyword=${encodeURIComponent(realContainerNo)}&page=1&size=20`)
|
||||
const preloadPage = context.apiSuccess(MODULE, '查询本轮预装清单', preloadPageResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '预装清单包含本轮已推送 EDITING 计划', preloadPageResponse,
|
||||
records(preloadPage).some((item) => id(item.planId) === planId && item.status === 'EDITING'),
|
||||
@@ -486,7 +464,7 @@ export async function run(context) {
|
||||
`${FIRST_LEG}/loading-plans/${planId}?warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const confirmable = context.apiSuccess(MODULE, '计划级确认前读取即时版本与就绪门禁', confirmableResponse, '', OPTIONS)
|
||||
const confirmableVersion = Number(confirmable?.summary?.version)
|
||||
context.assert(MODULE, '补号关联放舱后整份计划允许确认且 PDA 尚未创建', confirmableResponse,
|
||||
context.assert(MODULE, '真实柜合并放舱后整份计划允许确认且 PDA 尚未创建', confirmableResponse,
|
||||
Number.isInteger(confirmableVersion) && confirmable?.summary?.status === 'EDITING'
|
||||
&& confirmable?.summary?.readiness?.canConfirm === true
|
||||
&& confirmable?.summary?.readiness?.readyForPda === false
|
||||
|
||||
@@ -85,10 +85,11 @@ test('existing container number replacement keeps container plan and PDA identit
|
||||
assert.match(containerLoadingAcceptance, /pdaIdentityStable: true/)
|
||||
})
|
||||
|
||||
test('PDA task opens only after no-number push, transport, release and plan-level confirmation', () => {
|
||||
test('PDA task opens only after no-number push, real-container merge, release and plan-level confirmation', () => {
|
||||
assert.match(containerLoadingAcceptance, /无真实柜号推送预装计划成功/)
|
||||
assert.match(containerLoadingAcceptance, /无号 push 成功后 PDA 查询 A0404/)
|
||||
assert.match(containerLoadingAcceptance, /预装 EDITING 清单绑定本轮真实柜号/)
|
||||
assert.match(containerLoadingAcceptance, /realContainerBookingId: id\(requirement\.containerBookingId\)/)
|
||||
assert.match(containerLoadingAcceptance, /合并后列表仅保留模拟柜原地转成的一条 ACTIVE 操作身份/)
|
||||
assert.match(containerLoadingAcceptance, /订舱已关联但未 RELEASED 不生成 PDA 任务/)
|
||||
assert.match(containerLoadingAcceptance, /ocean-bookings\/\$\{bookingId\}\/release/)
|
||||
assert.match(containerLoadingAcceptance, /订舱携带已冻结运输快照放舱并满足 PDA 门禁/)
|
||||
|
||||
@@ -120,11 +120,12 @@ export async function run(context) {
|
||||
const base = `/api/platform/v1/inbounds/${inboundId}`
|
||||
const suffix = context.marker.replace(/\D/g, '').slice(-10)
|
||||
const warehouseId = id(initial?.summary?.warehouseId)
|
||||
const containerNo = `P3B-${suffix}`.slice(0, 64)
|
||||
const smartLoadingBase = '/api/platform/v1/first-leg/smart-loading'
|
||||
const pdaBase = '/api/platform/v1/first-leg/pda-loading'
|
||||
const bookingDraft = {
|
||||
customerId: tenantId, customerName: initial?.summary?.customerName,
|
||||
warehouseId, warehouseName: initial?.summary?.warehouseName,
|
||||
warehouseId,
|
||||
channelCode: initial?.summary?.channelCode,
|
||||
billOfLadingNo: `P3BL-${suffix}`.slice(0, 128),
|
||||
vesselName: `P3VESSEL-${suffix}`.slice(0, 128), voyageNo: `P3V-${suffix}`.slice(0, 64),
|
||||
@@ -135,12 +136,12 @@ 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}` }],
|
||||
containerRequirements: [{ containerType: '40HQ', containerKind: 'FCL', capacityCbm: 100,
|
||||
maxWeightKg: 100000, quantity: 1 }],
|
||||
containers: [{ containerNo, containerType: '40HC', containerKind: 'FCL', capacityCbm: 76,
|
||||
maxWeightKg: 28000 }],
|
||||
}
|
||||
const simulationResponse = await request(context, 'POST', '/api/platform/v1/containers', {
|
||||
channelCode: initial?.summary?.channelCode, warehouseId, containerType: '40HQ',
|
||||
capacityCbm: 100, maxWeightKg: 100000,
|
||||
channelCode: initial?.summary?.channelCode, warehouseId, containerType: '40HC',
|
||||
capacityCbm: 76, maxWeightKg: 28000,
|
||||
})
|
||||
const simulation = context.apiSuccess(MODULE, '新增三期内部模拟柜', simulationResponse, '', OPTIONS)
|
||||
const containerBookingId = id(simulation?.id)
|
||||
@@ -159,7 +160,6 @@ export async function run(context) {
|
||||
`candidate=${Boolean(candidate)}, warehouseId=${warehouseId || 'missing'}`, OPTIONS)
|
||||
if (!candidate?.inboundDetailId || !warehouseId) return
|
||||
|
||||
const containerNo = `P3B-${suffix}`
|
||||
|
||||
const loadingPlanResponse = await request(context, 'POST', `${smartLoadingBase}/plans`, {
|
||||
bookingId: containerBookingId, remark: context.marker,
|
||||
@@ -278,21 +278,9 @@ export async function run(context) {
|
||||
`changed=${rejectedDiff.join('|') || 'none'}; sameKey=${responseEvidence(sameKeyDifferentBody)}; newKey=${responseEvidence(newKeyDuplicate)}; task=${responseEvidence(rejectedTaskResponse)}`, OPTIONS)
|
||||
if (rejectedDiff.length !== 0 || codeOf(rejectedTaskResponse) !== 'A0404') return
|
||||
|
||||
const transportResponse = await request(context, 'PATCH',
|
||||
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}/transport?warehouseId=${encodeURIComponent(warehouseId)}`, {
|
||||
containerNo, voyageNo: `P3V-${suffix}`, expectedVersion: Number(postPushPlan.version),
|
||||
operationReason: `${context.marker}-bind-container-no`,
|
||||
}, { 'Idempotency-Key': `${context.marker}-preload-transport`.slice(0, 128) })
|
||||
const transported = context.apiSuccess(MODULE, '预装 EDITING 清单绑定真实柜号', transportResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '预装补号保持 loadingPlanId/containerBookingId/cabinetNo 身份稳定', transportResponse,
|
||||
transported?.summary?.containerNo === containerNo && id(transported?.summary?.planId) === loadingPlanId
|
||||
&& id(transported?.summary?.containerBookingId) === containerBookingId,
|
||||
`containerNo=${transported?.summary?.containerNo || 'missing'}, loadingPlanId=${id(transported?.summary?.planId) || 'missing'}`, OPTIONS)
|
||||
if (transported?.summary?.containerNo !== containerNo) return
|
||||
|
||||
const unlinkedTaskResponse = await context.request('GET',
|
||||
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
context.addStep(MODULE, '已补号但未关联订舱仍无 PDA 任务', unlinkedTaskResponse,
|
||||
context.addStep(MODULE, '无号且未关联订舱仍无 PDA 任务', unlinkedTaskResponse,
|
||||
unlinkedTaskResponse.http === 200 && codeOf(unlinkedTaskResponse) === 'A0404',
|
||||
`HTTP=${unlinkedTaskResponse.http}, code=${codeOf(unlinkedTaskResponse)}`, OPTIONS)
|
||||
|
||||
@@ -306,18 +294,18 @@ export async function run(context) {
|
||||
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?.containerRequirements?.find?.((item) => item.containerType === '40HQ')
|
||||
const requirement = bookingConfirmed?.containers?.find?.((item) => item.containerNo === containerNo)
|
||||
context.assert(MODULE, 'CONFIRMED 订舱回显完整且冻结五项运输快照', bookingCreateResponse,
|
||||
Boolean(bookingId) && frozenTransportMatches && Boolean(requirement?.id)
|
||||
&& bookingConfirmed?.containerAllocations?.length === 0,
|
||||
Boolean(bookingId) && frozenTransportMatches && Boolean(requirement?.containerBookingId)
|
||||
&& bookingConfirmed?.containers?.length === 1,
|
||||
`bookingId=${bookingId || 'missing'}, status=${bookingConfirmed?.oceanBooking?.status || 'missing'}`, OPTIONS)
|
||||
if (!bookingId || !requirement?.id || !frozenTransportMatches) return
|
||||
if (!bookingId || !requirement?.containerBookingId || !frozenTransportMatches) return
|
||||
const latestContainerResponse = await context.request('GET', `/api/platform/v1/containers/${containerBookingId}`)
|
||||
const latestContainer = context.apiSuccess(MODULE, '补号后读取模拟柜最新版本用于订舱关联', latestContainerResponse, '', OPTIONS)
|
||||
const latestContainer = context.apiSuccess(MODULE, '合并前读取模拟柜最新版本', latestContainerResponse, '', OPTIONS)
|
||||
const linkResponse = await request(context, 'POST', `/api/platform/v1/containers/${containerBookingId}/ocean-booking-link`, {
|
||||
oceanBookingId: bookingId, requirementId: id(requirement.id), expectedVersion: Number(latestContainer?.version),
|
||||
realContainerBookingId: id(requirement.containerBookingId), expectedVersion: Number(latestContainer?.version),
|
||||
})
|
||||
const linked = context.apiSuccess(MODULE, '预装补号后关联三期 CONFIRMED 订舱', linkResponse, '', OPTIONS)
|
||||
const linked = context.apiSuccess(MODULE, '模拟柜原地合并三期 CONFIRMED 真实订舱柜', linkResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '订舱关联保持柜次计划与真实柜号身份', linkResponse,
|
||||
id(linked?.id) === containerBookingId && linked?.cabinetNo === simulation.cabinetNo
|
||||
&& linked?.containerNo === containerNo && id(linked?.oceanBookingId) === bookingId,
|
||||
@@ -337,7 +325,8 @@ export async function run(context) {
|
||||
const bookingReleased = context.apiSuccess(MODULE, '三期订舱携带已冻结运输快照并放舱', releaseResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '三期订舱 RELEASED 后保持模拟柜关联身份', releaseResponse,
|
||||
bookingReleased?.oceanBooking?.status === 'RELEASED'
|
||||
&& bookingReleased?.containerAllocations?.some?.((item) => id(item.containerBookingId) === containerBookingId)
|
||||
&& 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
|
||||
@@ -352,7 +341,7 @@ export async function run(context) {
|
||||
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const confirmable = context.apiSuccess(MODULE, '计划级确认前读取即时版本与就绪门禁', confirmableResponse, '', OPTIONS)
|
||||
const confirmableVersion = Number(confirmable?.summary?.version)
|
||||
context.assert(MODULE, '补号关联放舱后整份计划允许确认且 PDA 尚未创建', confirmableResponse,
|
||||
context.assert(MODULE, '真实柜合并放舱后整份计划允许确认且 PDA 尚未创建', confirmableResponse,
|
||||
Number.isInteger(confirmableVersion) && confirmable?.summary?.status === 'EDITING'
|
||||
&& confirmable?.summary?.readiness?.canConfirm === true
|
||||
&& confirmable?.summary?.readiness?.readyForPda === false
|
||||
|
||||
@@ -14,6 +14,7 @@ test('each remainder uses a simulation linked to a confirmed then released booki
|
||||
assert.match(createPayload, /customerId: source\.tenantId/)
|
||||
assert.match(createPayload, /channelCode: source\.channelCode/)
|
||||
assert.match(source, /ocean-booking-link/)
|
||||
assert.match(source, /realContainerBookingId: id\(requirement\.containerBookingId\)/)
|
||||
assert.match(source, /bookingDetail\.oceanBooking\.status === 'CONFIRMED'/)
|
||||
assert.match(source, /`\$\{bookingPath\}\/release`/)
|
||||
const releaseStart = source.indexOf('const releaseResponse')
|
||||
@@ -39,18 +40,18 @@ test('release stops unless creation response and detail expose the same complete
|
||||
assert.match(source, /if \(!transportFrozen\) return null/)
|
||||
})
|
||||
|
||||
test('PDA opens only after a real container number and uses stable plan identity', () => {
|
||||
test('PDA opens only after merging a registered real container and uses stable plan identity', () => {
|
||||
const pushIndex = source.indexOf("`${SMART}/plans/${planId}/push-preload`")
|
||||
const transportIndex = source.indexOf("`${CONTAINERS}/${encodeURIComponent(containerId)}/transport`")
|
||||
const linkIndex = source.indexOf("`${CONTAINERS}/${containerId}/ocean-booking-link`")
|
||||
const releaseIndex = source.indexOf("`${bookingPath}/release`")
|
||||
const confirmIndex = source.indexOf("`/api/platform/v1/first-leg/loading-plans/${encodeURIComponent(planId)}/confirm`")
|
||||
const taskIndex = source.indexOf("`${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`")
|
||||
const canonicalTaskIndex = source.indexOf("`${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`", taskIndex + 1)
|
||||
assert.ok(transportIndex >= 0 && releaseIndex > transportIndex && pushIndex > releaseIndex
|
||||
assert.ok(linkIndex >= 0 && releaseIndex > linkIndex && pushIndex > releaseIndex
|
||||
&& taskIndex > pushIndex && confirmIndex > taskIndex && canonicalTaskIndex > confirmIndex)
|
||||
assert.match(source, /containerNo: realContainerNo/)
|
||||
assert.match(source, /const transportStable = transported\?\.containerNo === realContainerNo/)
|
||||
assert.match(source, /transported\?\.cabinetNo === simulation\.cabinetNo/)
|
||||
assert.match(source, /const realContainerNo = requirement\?\.containerNo/)
|
||||
assert.match(source, /const transportStable = linked\?\.containerNo === realContainerNo/)
|
||||
assert.match(source, /linked\?\.cabinetNo === simulation\.cabinetNo/)
|
||||
assert.match(source, /if \(!transportStable\) return null/)
|
||||
assert.match(source, /计划确认前不生成 PDA 任务/)
|
||||
assert.match(source, /summary\?\.readiness\?\.canConfirm === true/)
|
||||
@@ -92,8 +93,8 @@ test('booking freezes the source customer channel and warehouse scope', () => {
|
||||
assert.match(source, /customerId: source\.tenantId/)
|
||||
assert.match(source, /channelCode: source\.channelCode/)
|
||||
assert.match(source, /warehouseId: source\.warehouseId/)
|
||||
assert.match(source, /containerType === '40HQ'/)
|
||||
assert.match(source, /containerType: '40HQ', capacityCbm: 68, maxWeightKg: 28000/)
|
||||
assert.match(source, /containerType === '40HC'/)
|
||||
assert.match(source, /containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000/)
|
||||
})
|
||||
|
||||
test('booking fixture never invents supplier names without formal IDs', () => {
|
||||
|
||||
@@ -148,7 +148,7 @@ async function loadOneFragment(context, state) {
|
||||
const { tenantId, rootOrderId, sourceOrderId, sourceOrderNo, warehouseId, sourcePackageCount, sequence } = state
|
||||
const operationBase = `${context.marker}-chain-${sequence}`.slice(0, 100)
|
||||
const simulationResponse = await request(context, 'POST', CONTAINERS, {
|
||||
channelCode: state.channelCode, warehouseId, containerType: '40HQ', capacityCbm: 68, maxWeightKg: 28000,
|
||||
channelCode: state.channelCode, warehouseId, containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000,
|
||||
})
|
||||
const simulation = context.apiSuccess(MODULE, `为 ${sourceOrderNo} 新增内部模拟柜`, simulationResponse, '', OPTIONS)
|
||||
const containerId = id(simulation?.id)
|
||||
@@ -164,12 +164,13 @@ async function loadOneFragment(context, state) {
|
||||
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
|
||||
&& bookingConfirmed?.containers?.length === 1,
|
||||
`status=${bookingConfirmed?.oceanBooking?.status || 'missing'}, containers=${bookingConfirmed?.containers?.length ?? 'n/a'}`, OPTIONS)
|
||||
const requirement = bookingConfirmed?.containers?.find?.((item) => item.containerType === '40HC')
|
||||
const realContainerNo = requirement?.containerNo
|
||||
if (!requirement?.containerBookingId || !realContainerNo) return null
|
||||
const linkResponse = await request(context, 'POST', `${CONTAINERS}/${containerId}/ocean-booking-link`, {
|
||||
oceanBookingId, requirementId: id(requirement.id), expectedVersion: Number(simulation.version),
|
||||
realContainerBookingId: id(requirement.containerBookingId), expectedVersion: Number(simulation.version),
|
||||
})
|
||||
const linked = context.apiSuccess(MODULE, `关联 ${sourceOrderNo} 的模拟柜与真实订舱`, linkResponse, '', OPTIONS)
|
||||
context.assert(MODULE, `${sourceOrderNo} 关联后保持稳定内部柜次`, linkResponse,
|
||||
@@ -213,29 +214,21 @@ async function loadOneFragment(context, state) {
|
||||
isConflict(staleAssign) && hasTrace(staleAssign),
|
||||
`HTTP=${staleAssign.http}, code=${codeOf(staleAssign)}, requestId=${staleAssign.requestId || 'missing'}, traceId=${staleAssign.traceId || 'missing'}`, OPTIONS)
|
||||
|
||||
const containerDetailResponse = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(containerId)}`)
|
||||
const containerDetail = context.apiSuccess(MODULE, `回查 ${sourceOrderNo} 的自动柜次`, containerDetailResponse, '', OPTIONS)
|
||||
const realContainerNo = `SPLIT-${operationBase.replace(/[^A-Za-z0-9]/g, '').slice(-18)}`.slice(0, 64)
|
||||
const transportResponse = await request(context, 'PATCH', `${CONTAINERS}/${encodeURIComponent(containerId)}/transport`, {
|
||||
containerNo: realContainerNo, voyageNo: `V-${sequence}`,
|
||||
version: Number(containerDetail?.version), reason: context.marker,
|
||||
})
|
||||
const transported = context.apiSuccess(MODULE, `为 ${sourceOrderNo} 补充真实柜号并开放 PDA`, transportResponse, '', OPTIONS)
|
||||
const transportStable = transported?.containerNo === realContainerNo && id(transported?.id) === containerId
|
||||
&& transported?.cabinetNo === simulation.cabinetNo
|
||||
context.assert(MODULE, `${sourceOrderNo} 真实柜号已绑定到稳定柜次`, transportResponse,
|
||||
const transportStable = linked?.containerNo === realContainerNo && id(linked?.id) === containerId
|
||||
&& linked?.cabinetNo === simulation.cabinetNo
|
||||
context.assert(MODULE, `${sourceOrderNo} 合并后真实柜号落在稳定模拟柜身份`, linkResponse,
|
||||
transportStable,
|
||||
`containerBookingId=${containerId}, containerNo=${transported?.containerNo || 'missing'}, cabinetNo=${transported?.cabinetNo || 'missing'}`, OPTIONS)
|
||||
`containerBookingId=${containerId}, containerNo=${linked?.containerNo || 'missing'}, cabinetNo=${linked?.cabinetNo || 'missing'}`, OPTIONS)
|
||||
if (!transportStable) return null
|
||||
|
||||
const bookingPath = `/api/platform/v1/ocean-bookings/${encodeURIComponent(oceanBookingId)}`
|
||||
let bookingDetailResponse = await context.request('GET', bookingPath)
|
||||
let bookingDetail = context.apiSuccess(MODULE, `回查 ${sourceOrderNo} 柜次所属真实订舱`, bookingDetailResponse, '', OPTIONS)
|
||||
const allocationMatches = bookingDetail?.containerAllocations?.some?.((item) =>
|
||||
const allocationMatches = bookingDetail?.containers?.some?.((item) =>
|
||||
id(item?.containerBookingId) === containerId)
|
||||
context.assert(MODULE, `${sourceOrderNo} 订舱分配保持稳定柜次`, bookingDetailResponse,
|
||||
id(bookingDetail?.oceanBooking?.id) === oceanBookingId && allocationMatches,
|
||||
`oceanBookingId=${id(bookingDetail?.oceanBooking?.id) || 'missing'}, containerBookingId=${containerId}, allocations=${bookingDetail?.containerAllocations?.length ?? 'n/a'}`, OPTIONS)
|
||||
`oceanBookingId=${id(bookingDetail?.oceanBooking?.id) || 'missing'}, containerBookingId=${containerId}, containers=${bookingDetail?.containers?.length ?? 'n/a'}`, OPTIONS)
|
||||
if (!allocationMatches || id(bookingDetail?.oceanBooking?.id) !== oceanBookingId) return null
|
||||
|
||||
const createdTransport = bookingTransportSnapshot(bookingCreated?.oceanBooking)
|
||||
@@ -262,7 +255,7 @@ async function loadOneFragment(context, state) {
|
||||
bookingDetailResponse = await context.request('GET', bookingPath)
|
||||
bookingDetail = context.apiSuccess(MODULE, `回查 ${sourceOrderNo} 的 RELEASED 订舱`, bookingDetailResponse, '', OPTIONS)
|
||||
}
|
||||
const releasedAllocationMatches = bookingDetail?.containerAllocations?.some?.((item) =>
|
||||
const releasedAllocationMatches = bookingDetail?.containers?.some?.((item) =>
|
||||
id(item?.containerBookingId) === containerId)
|
||||
context.assert(MODULE, `${sourceOrderNo} 仅在 RELEASED 且柜次分配稳定后推送`, bookingDetailResponse,
|
||||
bookingDetail?.oceanBooking?.status === 'RELEASED' && releasedAllocationMatches,
|
||||
|
||||
@@ -42,7 +42,6 @@ async function request(context, method, path, body, headers = {}) {
|
||||
function oceanDraft(context, suffix, master) {
|
||||
return {
|
||||
...oceanBookingDraft(context, suffix, master),
|
||||
shippingCompanyName: `船期船司-${suffix}`, bookingAgentName: `船期代理-${suffix}`,
|
||||
departurePortCode: 'CNSZX',
|
||||
departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
|
||||
placeOfDelivery: 'LAX', serviceTerm: 'CY-CY', freightTerm: 'NINGBO',
|
||||
@@ -52,7 +51,6 @@ function oceanDraft(context, suffix, master) {
|
||||
{ partyType: 'CONSIGNEE', partyName: `收货方-${suffix}` },
|
||||
{ partyType: 'NOTIFY_PARTY', partyName: `通知方-${suffix}` },
|
||||
],
|
||||
containerRequirements: [{ containerType: '40HQ', containerKind: 'FCL', capacityCbm: 68, maxWeightKg: 28000, quantity: 1 }],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +63,7 @@ async function ensureOceanBooking(context, suffix, master) {
|
||||
return detail?.oceanBooking ? {
|
||||
id: existingId,
|
||||
customerId: id(detail.oceanBooking.customerId),
|
||||
allocatedContainerBookingIds: (detail.containerAllocations || [])
|
||||
allocatedContainerBookingIds: (detail.containers || [])
|
||||
.map((item) => id(item?.containerBookingId)).filter(Boolean),
|
||||
} : null
|
||||
}
|
||||
@@ -78,7 +76,7 @@ async function ensureOceanBooking(context, suffix, master) {
|
||||
`oceanBookingId=${oceanBookingId || 'missing'}, customerId=${id(detail?.oceanBooking?.customerId) || 'missing'}`, OPTIONS)
|
||||
return oceanBookingId ? {
|
||||
id: oceanBookingId, customerId: id(detail.oceanBooking.customerId),
|
||||
allocatedContainerBookingIds: (detail.containerAllocations || [])
|
||||
allocatedContainerBookingIds: (detail.containers || [])
|
||||
.map((item) => id(item?.containerBookingId)).filter(Boolean),
|
||||
} : null
|
||||
}
|
||||
|
||||
@@ -90,9 +90,8 @@ test('bridge loading confirms preload before creating and claiming the PDA task'
|
||||
'无真实柜号推送预装成功并冻结柜次明细',
|
||||
'无号 push 成功后 PDA 查询明确 A0404',
|
||||
'push exact replay 对计划版本历史关联和任务零漂移',
|
||||
'预装 EDITING 清单绑定真实柜号',
|
||||
'已补号但未关联订舱仍无 PDA 任务',
|
||||
'预装补号后关联三期 CONFIRMED 订舱',
|
||||
'无号且未关联订舱仍无 PDA 任务',
|
||||
'模拟柜原地合并三期 CONFIRMED 真实订舱柜',
|
||||
'订舱已关联但未 RELEASED 仍无 PDA 任务',
|
||||
'三期订舱携带已冻结运输快照并放舱',
|
||||
'订舱 RELEASED 但预装未 CONFIRMED 仍无 PDA 任务',
|
||||
@@ -244,38 +243,36 @@ test('container loading arranges a simulation before linking a confirmed real bo
|
||||
"const planResponse = await request(context, 'POST', `${SMART}/plans`",
|
||||
"const assignResponse = await request",
|
||||
'无真实柜号推送预装计划成功',
|
||||
'预装 EDITING 清单绑定本轮真实柜号',
|
||||
"const bookingCreate = 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, /warehouseName\s*:/)
|
||||
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, /expectedVersion: Number\(postPushPlan\.version\), operationReason: context\.marker/)
|
||||
assert.match(value, /confirmedBooking\.containers\.length === 2/)
|
||||
assert.match(value, /realContainerBookingId: id\(requirement\.containerBookingId\)/)
|
||||
assert.match(value, /function responseEvidence\(response\)[\s\S]*HTTP=\$\{response\?\.http \?\? 'missing'\}[\s\S]*code=\$\{codeOf\(response\)\}[\s\S]*requestId=\$\{response\?\.requestId \|\| 'missing'\}[\s\S]*traceId=\$\{response\?\.traceId \|\| 'missing'\}/)
|
||||
assert.match(value, /emptySimulation = context\.apiSuccess\(MODULE, '空模拟柜首次关联真实订舱', emptyLink,\s+responseEvidence\(emptyLink\), OPTIONS\)\s+if \(!emptySimulation\) return\s+const emptyUnlink = await request/)
|
||||
assert.match(value, /emptySimulation = context\.apiSuccess\(MODULE, '空模拟柜允许解除关联', emptyUnlink,\s+responseEvidence\(emptyUnlink\), OPTIONS\)\s+if \(!emptySimulation\) return\s+const emptyRelink = await request/)
|
||||
assert.match(value, /emptySimulation = context\.apiSuccess\(MODULE, '空模拟柜解绑后允许重新关联', emptyRelink,\s+responseEvidence\(emptyRelink\), OPTIONS\)\s+if \(!emptySimulation\) return\s+const emptyFinalUnlink = await request/)
|
||||
assert.match(value, /const emptyReleased = context\.apiSuccess\(MODULE, '空模拟柜最终解绑释放真实订舱容量', emptyFinalUnlink,\s+responseEvidence\(emptyFinalUnlink\), OPTIONS\)\s+if \(!emptyReleased\) return\s+const beforeLinkItems/)
|
||||
assert.match(value, /合并后列表仅保留模拟柜原地转成的一条 ACTIVE 操作身份/)
|
||||
assert.match(value, /订舱真实柜 allocation 已原子改指稳定模拟柜身份/)
|
||||
assert.match(value, /已消费 MERGED 真实柜拒绝再次关联/)
|
||||
assertOrder(value, [
|
||||
"const linkedContainer = context.apiSuccess(MODULE, '首次关联真实已确认订舱柜', linkResponse,",
|
||||
'if (!linkedContainer) return',
|
||||
"const afterLinkPlanResponse = await context.request('GET'",
|
||||
"const duplicateSimulationResponse = await request(context, 'POST', CONTAINERS",
|
||||
])
|
||||
assert.equal(value.match(/responseEvidence\((?:emptyLink|emptyUnlink|emptyRelink|emptyFinalUnlink|linkResponse)\)/g)?.length, 5)
|
||||
assert.match(value, /realContainerBookingId: id\(requirement\.containerBookingId\)/)
|
||||
assert.match(value, /关联前后内部柜次、计划和排货明细身份稳定/)
|
||||
assert.match(value, /bookingId=\$\{encodeURIComponent\(containerId\)\}&warehouseId=/)
|
||||
assert.match(value, /tasks\?loadingPlanId=\$\{encodeURIComponent\(planId\)\}&warehouseId=/)
|
||||
assert.match(value, /loadingPlanId: planId, containerBookingId: containerId/)
|
||||
assert.ok(value.includes('`${FIRST_LEG}/loading-plans/${planId}/transport?warehouseId='))
|
||||
assert.match(value, /预装 EDITING 清单绑定本轮真实柜号/)
|
||||
assert.doesNotMatch(value.slice(value.indexOf('无真实柜号推送预装计划成功'), value.indexOf('const linkResponse')),
|
||||
/\/transport/)
|
||||
assert.match(value, /订舱携带已冻结运输快照放舱并满足 PDA 门禁/)
|
||||
assert.match(value, /无真实柜号推送预装计划成功/)
|
||||
assert.match(value, /订舱 RELEASED 但预装未 CONFIRMED 不生成 PDA 任务/)
|
||||
@@ -360,8 +357,7 @@ test('container loading enforces channel isolation and stable PDA identity acros
|
||||
'人工分配本轮候选货物',
|
||||
'无真实柜号推送预装计划成功',
|
||||
'无号 push 成功后 PDA 查询 A0404',
|
||||
'预装 EDITING 清单绑定本轮真实柜号',
|
||||
'已补号但未关联订舱不生成 PDA 任务',
|
||||
'无号且未关联订舱不生成 PDA 任务',
|
||||
'首次关联真实已确认订舱柜',
|
||||
'订舱已关联但未 RELEASED 不生成 PDA 任务',
|
||||
'订舱携带已冻结运输快照放舱并满足 PDA 门禁',
|
||||
@@ -501,7 +497,7 @@ test('downstream producers prefer the phase3Loading container fixture', () => {
|
||||
assert.doesNotMatch(serviceCenter, /链路承运商-/)
|
||||
assert.doesNotMatch(serviceCenter, /customerName: `服务中心链路客户-/)
|
||||
const schedules = source('./phase3-port-schedules.mjs')
|
||||
assert.match(schedules, /allocatedContainerBookingIds: \(detail\.containerAllocations \|\| \[\]\)/)
|
||||
assert.match(schedules, /allocatedContainerBookingIds: \(detail\.containers \|\| \[\]\)/)
|
||||
assert.match(schedules, /ocean\.allocatedContainerBookingIds\?\.includes\(containerBookingId\)/)
|
||||
assert.match(schedules, /if \(containerBookingId && !knownAllocation\)/)
|
||||
})
|
||||
|
||||
@@ -64,10 +64,10 @@ export async function run(context) {
|
||||
return
|
||||
}
|
||||
if (linkContainer) {
|
||||
const allocationMatches = oceanDetail?.containerAllocations?.some?.((item) =>
|
||||
const allocationMatches = oceanDetail?.containers?.some?.((item) =>
|
||||
id(item?.containerBookingId) === id(containerFlow.containerBookingId))
|
||||
context.assert(MODULE, '已放舱订舱保留本轮柜次分配', oceanResponse, allocationMatches,
|
||||
`containerBookingId=${id(containerFlow.containerBookingId)}, allocations=${oceanDetail?.containerAllocations?.length ?? 'n/a'}`, OPTIONS)
|
||||
`containerBookingId=${id(containerFlow.containerBookingId)}, containers=${oceanDetail?.containers?.length ?? 'n/a'}`, OPTIONS)
|
||||
if (!allocationMatches) return
|
||||
}
|
||||
const payload = {
|
||||
|
||||
Reference in New Issue
Block a user