test(firstleg): cover simulated container booking links

This commit is contained in:
2026-08-14 21:00:20 +08:00
parent 7184f5083a
commit a4a4374c67
14 changed files with 318 additions and 81 deletions
@@ -108,6 +108,7 @@ 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 bookingDraft = {
customerId: tenantId, customerName: inbound?.summary?.customerName || `验收客户-${tenantId}`,
channelCode, warehouseId, warehouseName,
@@ -115,31 +116,27 @@ export async function run(context) {
bookingAgentId: null, bookingAgentName: `验收代理-${suffix}`,
departurePortCode: 'CNSZX', departurePortName: '深圳',
destinationPortCode: 'USLAX', destinationPortName: '洛杉矶', placeOfDelivery: 'LAX',
serviceTerm: 'CY/CY', freightTerm: 'PREPAID', remark: context.marker,
serviceTerm: 'CY-CY', freightTerm: 'NINGBO', remark: context.marker,
parties: [{ partyType: 'SHIPPER', partyName: `发货方-${suffix}` },
{ partyType: 'CONSIGNEE', partyName: `收货方-${suffix}` },
{ partyType: 'NOTIFY_PARTY', partyName: `通知方-${suffix}` }],
containerRequirements: [{ containerType: '40HQ', containerKind: 'FCL', capacityCbm: 100, maxWeightKg: 100000, quantity: 1 }],
containerRequirements: [primaryRequirement,
{ containerType: '20GP', containerKind: 'FCL', capacityCbm: 28, maxWeightKg: 22000, quantity: 1 }],
}
context.assert(MODULE, '订舱渠道与仓库快照完整', inboundResponse, Boolean(channelCode && channelName && warehouseName),
`channelCode=${channelCode || 'missing'}, warehouseId=${warehouseId}, warehouseName=${warehouseName || 'missing'}`, OPTIONS)
const bookingCreate = await request(context, 'POST', '/api/platform/v1/ocean-bookings',
{ expectedVersion: 0, data: bookingDraft }, { 'Idempotency-Key': `${context.marker}-loading-booking-create` })
const bookingCreated = context.apiSuccess(MODULE, '创建含渠道仓库快照的隔离订舱', bookingCreate, '', OPTIONS)
const bookingId = id(bookingCreated?.oceanBooking?.id)
if (!bookingId) return
const bookingVersion = Number(bookingCreated.oceanBooking.version)
const submit = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/submit`,
{ expectedVersion: bookingVersion, reason: context.marker }, { 'Idempotency-Key': `${context.marker}-loading-booking-submit` })
const submitted = context.apiSuccess(MODULE, '提交隔离订舱', submit, '', 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)
const allocation = confirmedBooking?.containerAllocations?.[0]
const containerId = id(allocation?.containerBookingId)
context.assert(MODULE, '确认响应包含自动生成的真实柜次', confirm,
Boolean(containerId) && id(confirmedBooking?.oceanBooking?.id) === bookingId,
`bookingId=${bookingId}, containerBookingId=${containerId || 'missing'}`, OPTIONS)
const simulationResponse = await request(context, 'POST', CONTAINERS, {
channelCode, warehouseId, containerType: primaryRequirement.containerType,
capacityCbm: primaryRequirement.capacityCbm, maxWeightKg: primaryRequirement.maxWeightKg,
})
const simulation = context.apiSuccess(MODULE, '新增内部模拟柜', simulationResponse,
`warehouseId=${warehouseId}, channelCode=${channelCode}`, OPTIONS)
const containerId = id(simulation?.id)
context.assert(MODULE, '模拟柜由服务端生成稳定身份且不伪造真实柜号或订舱', simulationResponse,
Boolean(containerId) && Boolean(simulation?.cabinetNo) && simulation?.containerNo == null
&& simulation?.oceanBookingId == null && simulation?.channelCode === channelCode
&& id(simulation?.warehouseId) === warehouseId,
`containerBookingId=${containerId || 'missing'}, cabinetNo=${simulation?.cabinetNo || 'missing'}, oceanBookingId=${id(simulation?.oceanBookingId) || 'null'}`, OPTIONS)
if (!containerId) return
const candidateResponse = await context.request('GET', `${SMART}/candidates?bookingId=${encodeURIComponent(containerId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
@@ -160,22 +157,12 @@ export async function run(context) {
`${channelIsolation.reason}; originWarehouseId=${warehouseId}, primaryChannelCode=${channelCode}`, OPTIONS)
} else {
const foreignChannel = channelIsolation.pair.foreign
const foreignBookingCreate = await request(context, 'POST', '/api/platform/v1/ocean-bookings', {
expectedVersion: 0,
data: { ...bookingDraft, channelCode: foreignChannel.channelCode, remark: `${context.marker}-other-channel` },
}, { 'Idempotency-Key': `${context.marker}-other-channel-booking-create`.slice(0, 128) })
const foreignBookingCreated = context.apiSuccess(MODULE, '创建真实异渠道隔离订舱', foreignBookingCreate, '', OPTIONS)
const foreignBookingId = id(foreignBookingCreated?.oceanBooking?.id)
if (!foreignBookingId) return
const foreignSubmit = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${foreignBookingId}/submit`, {
expectedVersion: Number(foreignBookingCreated?.oceanBooking?.version), reason: context.marker,
}, { 'Idempotency-Key': `${context.marker}-other-channel-booking-submit`.slice(0, 128) })
const foreignSubmitted = context.apiSuccess(MODULE, '提交真实异渠道隔离订舱', foreignSubmit, '', OPTIONS)
const foreignConfirm = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${foreignBookingId}/confirm`, {
expectedVersion: Number(foreignSubmitted?.oceanBooking?.version), reason: context.marker,
}, { 'Idempotency-Key': `${context.marker}-other-channel-booking-confirm`.slice(0, 128) })
const foreignConfirmed = context.apiSuccess(MODULE, '确认真实异渠道订舱并自动生成柜次', foreignConfirm, '', OPTIONS)
const foreignContainerId = id(foreignConfirmed?.containerAllocations?.[0]?.containerBookingId)
const foreignContainerResponse = await request(context, 'POST', CONTAINERS, {
channelCode: foreignChannel.channelCode, warehouseId, containerType: primaryRequirement.containerType,
capacityCbm: primaryRequirement.capacityCbm, maxWeightKg: primaryRequirement.maxWeightKg,
})
const foreignContainer = context.apiSuccess(MODULE, '新增真实异渠道模拟柜', foreignContainerResponse, '', OPTIONS)
const foreignContainerId = id(foreignContainer?.id)
if (!foreignContainerId) return
const foreignPlanResponse = await request(context, 'POST', `${SMART}/plans`, {
bookingId: foreignContainerId, remark: `${context.marker}-other-channel`,
@@ -240,6 +227,108 @@ export async function run(context) {
staleAssignResponse.http === 200 && ['A0409', '409'].includes(codeOf(staleAssignResponse)),
`HTTP=${staleAssignResponse.http}, code=${codeOf(staleAssignResponse)}`, OPTIONS)
const bookingCreate = await request(context, 'POST', '/api/platform/v1/ocean-bookings',
{ expectedVersion: 0, data: bookingDraft }, { 'Idempotency-Key': `${context.marker}-loading-booking-create` })
const bookingCreated = context.apiSuccess(MODULE, '创建含渠道仓库快照的真实订舱草稿', bookingCreate, '', OPTIONS)
const bookingId = id(bookingCreated?.oceanBooking?.id)
const 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,
confirmedBooking?.oceanBooking?.status === 'CONFIRMED'
&& Array.isArray(confirmedBooking?.containerAllocations) && confirmedBooking.containerAllocations.length === 0,
`status=${confirmedBooking?.oceanBooking?.status || 'missing'}, allocations=${confirmedBooking?.containerAllocations?.length ?? 'n/a'}`, OPTIONS)
const confirmedListResponse = await context.request('GET',
`/api/platform/v1/ocean-bookings?page=1&size=20&keyword=${encodeURIComponent(bookingNo)}`)
const confirmedList = context.apiSuccess(MODULE, '按真实订舱号查询正式列表', confirmedListResponse, '', OPTIONS)
context.assert(MODULE, 'CONFIRMED 后真实订舱进入正式列表', confirmedListResponse,
records(confirmedList).some((item) => id(item.id) === bookingId && item.status === 'CONFIRMED'),
`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 wrongTypeLinkResponse = await request(context, 'POST', `${CONTAINERS}/${containerId}/ocean-booking-link`, {
oceanBookingId: bookingId, requirementId: id(wrongTypeRequirement.id), expectedVersion: Number(containerDetail.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, '', OPTIONS)
const emptyUnlink = await request(context, 'DELETE', `${CONTAINERS}/${emptyContainerId}/ocean-booking-link`, {
expectedVersion: Number(emptySimulation.version), reason: context.marker,
})
emptySimulation = context.apiSuccess(MODULE, '空模拟柜允许解除关联', emptyUnlink, '', OPTIONS)
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, '', OPTIONS)
const emptyFinalUnlink = await request(context, 'DELETE', `${CONTAINERS}/${emptyContainerId}/ocean-booking-link`, {
expectedVersion: Number(emptySimulation.version), reason: `${context.marker}-release-capacity`,
})
context.apiSuccess(MODULE, '空模拟柜最终解绑释放真实订舱容量', emptyFinalUnlink, '', OPTIONS)
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(containerDetail.version),
})
const linkedContainer = context.apiSuccess(MODULE, '首次关联真实已确认订舱柜', linkResponse, '', OPTIONS)
const afterLinkPlanResponse = await context.request('GET', `${SMART}/plans/${planId}`)
const afterLinkPlan = context.apiSuccess(MODULE, '关联后回查原装柜计划与明细', afterLinkPlanResponse, '', OPTIONS)
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
&& beforeLinkItems === afterLinkItems,
`containerBookingId=${containerId}, cabinetNo=${linkedContainer?.cabinetNo || 'missing'}, loadingPlanId=${id(afterLinkPlan?.id) || '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),
})
context.addStep(MODULE, '同一真实订舱箱型容量拒绝重复关联', duplicateLinkResponse,
failure(duplicateLinkResponse), `HTTP=${duplicateLinkResponse.http}, code=${codeOf(duplicateLinkResponse)}`, OPTIONS)
const unlinkAfterLoadingResponse = await request(context, 'DELETE', `${CONTAINERS}/${containerId}/ocean-booking-link`, {
expectedVersion: Number(linkedContainer.version), reason: context.marker,
})
context.addStep(MODULE, '已有排货后拒绝解绑或换绑', unlinkAfterLoadingResponse,
failure(unlinkAfterLoadingResponse), `HTTP=${unlinkAfterLoadingResponse.http}, code=${codeOf(unlinkAfterLoadingResponse)}`, OPTIONS)
const pushKey = `${context.marker}-container-push`.slice(0, 128)
const pushResponse = await request(context, 'POST', `${SMART}/plans/${planId}/push-preload`,
{ expectedVersion: Number(assigned.version) }, { 'Idempotency-Key': pushKey })