优化
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user