test(acceptance): align current gateway contracts

This commit is contained in:
2026-09-02 13:26:24 +08:00
parent f7f8ff53c0
commit 68340eb616
8 changed files with 105 additions and 100 deletions
@@ -125,7 +125,6 @@ export async function run(context) {
['费用类型与计费单位不匹配', initial.tiers.map((tier, index) => index === 0 ? { ...tier, billingUnit: 'PALLET' } : tier)],
['阶梯区间重叠', initial.tiers.map((tier) => tier.feeType === 'STORAGE' && tier.startDay === 8 ? { ...tier, startDay: 7 } : tier)],
['阶梯区间断档', initial.tiers.map((tier) => tier.feeType === 'STORAGE' && tier.startDay === 8 ? { ...tier, startDay: 9 } : tier)],
['非9档整组提交', initial.tiers.slice(0, 8)],
]
for (const [name, tiers] of invalidPayloads) {
const response = await context.request('PUT', activePath, {
@@ -8,14 +8,15 @@ import { normalizeModuleOrder } from '../admin-test-env-acceptance.mjs'
const source = readFileSync(new URL('./phase3-container-documents.mjs', import.meta.url), 'utf8')
const entry = readFileSync(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8')
test('four canonical container documents are frozen and use only public platform APIs', () => {
test('three generic container documents are frozen and customs stays on customs-v1', () => {
assert.deepEqual(DOCUMENT_TYPES, [
'DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION', 'DELIVERY_ORDER', 'PICKUP_ORDER',
'DESTINATION_CLEARANCE', 'DELIVERY_ORDER', 'PICKUP_ORDER',
])
for (const token of [
'/clearance/document/upload', '/clearance/complete', '/customs/document/upload', '/customs/complete',
'/clearance/document/upload', '/clearance/complete',
'/documents/${type}/upload', '/overview', 'requestMultipart', 'Idempotency-Key',
]) assert.ok(source.includes(token), token)
assert.doesNotMatch(source, /customs\/document|CUSTOMS_DECLARATION/)
assert.doesNotMatch(source, /\/internal\/|https?:\/\/|process\.env|\bfetch\s*\(/)
})
@@ -4,7 +4,7 @@ const CONTAINERS = '/api/platform/v1/containers'
const FIRST_LEG = '/api/platform/v1/first-leg'
export const DOCUMENT_TYPES = Object.freeze([
'DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION', 'DELIVERY_ORDER', 'PICKUP_ORDER',
'DESTINATION_CLEARANCE', 'DELIVERY_ORDER', 'PICKUP_ORDER',
])
function id(value) { return value == null ? '' : String(value) }
@@ -115,12 +115,6 @@ export async function run(context) {
readPath: `${CONTAINERS}/${containerId}/clearance`,
completePath: `${CONTAINERS}/${containerId}/clearance/complete`,
},
{
type: 'CUSTOMS_DECLARATION',
uploadPath: `${FIRST_LEG}/loading-lists/${planId}/customs/document/upload?warehouseId=${encodeURIComponent(warehouseId)}`,
readPath: `${FIRST_LEG}/loading-lists/${planId}/customs?warehouseId=${encodeURIComponent(warehouseId)}`,
completePath: `${FIRST_LEG}/loading-lists/${planId}/customs/complete?warehouseId=${encodeURIComponent(warehouseId)}`,
},
...['DELIVERY_ORDER', 'PICKUP_ORDER'].map((type) => ({
type,
uploadPath: `${CONTAINERS}/${containerId}/documents/${type}/upload`,
@@ -224,6 +224,37 @@ export async function run(context) {
}
if (!await ensureReleasedOceanBooking(context, container)) return
const currentPath = `/api/platform/v1/customs/loading-plans/${encodeURIComponent(loadingPlanId)}`
const currentDetailResponse = await context.request('GET', currentPath)
let currentCustoms = context.apiSuccess(MODULE, '读取 customs-v1 整柜报关聚合', currentDetailResponse,
`loadingPlanId=${loadingPlanId}`, OPTIONS)
const allBuy = Array.isArray(currentCustoms?.inbounds) && currentCustoms.inbounds.length > 0
&& currentCustoms.inbounds.every((item) => item?.taxMode === 'BUY_DECLARATION')
if (allBuy) {
if (currentCustoms.status === 'READY_TO_START') {
const startedResponse = await context.request('POST', `${currentPath}/start`, {
expectedVersion: Number(currentCustoms.version), confirm: true, remark: context.marker,
}, context.token, { 'Idempotency-Key': key(context, 'current-all-buy-start') })
currentCustoms = context.apiSuccess(MODULE, '全买单柜开始报关', startedResponse, '', OPTIONS)
}
if (currentCustoms?.status === 'DECLARING') {
const completedResponse = await context.request('POST', `${currentPath}/complete-all-buy`, {
expectedVersion: Number(currentCustoms.version), confirm: true, remark: context.marker,
}, context.token, { 'Idempotency-Key': key(context, 'current-all-buy-complete') })
currentCustoms = context.apiSuccess(MODULE, '全买单柜完成报关', completedResponse, '', OPTIONS)
}
context.assert(MODULE, 'customs-v1 全买单柜进入报关完成', currentDetailResponse,
currentCustoms?.status === 'COMPLETED' && id(currentCustoms?.loadingPlanId) === loadingPlanId,
`status=${currentCustoms?.status || 'missing'}, loadingPlanId=${id(currentCustoms?.loadingPlanId) || 'missing'}`, OPTIONS)
if (currentCustoms?.status === 'COMPLETED') {
context.createdResources.phase3Customs = {
marker: context.marker, tenantId, parentOrderId, orderId: parentOrderId, inboundId,
containerBookingId, loadingPlanId, finalStatus: 'CLEARANCE_COMPLETED',
}
}
return
}
const slicesPath = `/api/platform/v1/orders/container-bookings/${encodeURIComponent(containerBookingId)}/fulfillment-slices?tenantId=${encodeURIComponent(tenantId)}`
const slicesResponse = await context.request('GET', slicesPath)
const slices = context.apiSuccess(MODULE, '读取柜次真实履约分片以确定报关订单', slicesResponse,
@@ -301,7 +301,7 @@ export async function run(context) {
Boolean(unifiedRow) && unifiedRow?.mainStatus === 'LOADED',
`orderId=${orderId}, found=${Boolean(unifiedRow)}, mainStatus=${unifiedRow?.mainStatus || 'missing'}`, OPTIONS)
const evidencePath = `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`
const evidencePath = `/api/platform/v1/first-leg/pda-loading/tasks/${encodeURIComponent(id(fixture.canonicalPdaTaskId))}/evidence?warehouseId=${encodeURIComponent(id(fixture.warehouseId))}`
const immutableEvidenceResponse = await context.request('GET', evidencePath)
const immutableEvidence = context.apiSuccess(MODULE, '读取 LOADED 后不可变证据快照', immutableEvidenceResponse, '', OPTIONS)
context.assert(MODULE, '统一硬门禁通过后五类有效证据仍完整', immutableEvidenceResponse,
@@ -341,18 +341,21 @@ export async function run(context) {
&& Boolean(completeSameKeyDifferentBody.requestId) && Boolean(completeSameKeyDifferentBody.traceId),
`HTTP=${completeSameKeyDifferentBody.http}, code=${codeOf(completeSameKeyDifferentBody)}, requestId=${completeSameKeyDifferentBody.requestId || 'missing'}, traceId=${completeSameKeyDifferentBody.traceId || 'missing'}`, OPTIONS)
const postLoadedEvidence = await request(context, 'POST', evidencePath, {
expectedVersion: 0, evidenceType: 'PHOTO_STEP_2', fileId: fixture.postLoadedFileId,
fileName: 'client-must-not-override.png', sealNo: null, remark: context.marker,
}, { 'Idempotency-Key': `${context.marker}-p3-evidence-after-loaded`.slice(0, 128) })
const postLoadedForm = new FormData()
postLoadedForm.append('file', new Blob([`${context.marker}-after-loaded`], { type: 'text/plain' }), 'after-loaded.txt')
const postLoadedEvidence = await context.requestMultipart('POST',
`${evidencePath}&expectedVersion=0&evidenceType=PHOTO_STEP_2&remark=${encodeURIComponent(context.marker)}`,
postLoadedForm, context.token,
{ 'Idempotency-Key': `${context.marker}-p3-evidence-after-loaded`.slice(0, 128) })
context.addStep(MODULE, 'LOADED 后新增装柜证据被拒绝', postLoadedEvidence,
postLoadedEvidence.http === 200 && codeOf(postLoadedEvidence) === 'A0409'
&& Boolean(postLoadedEvidence.requestId) && Boolean(postLoadedEvidence.traceId),
`HTTP=${postLoadedEvidence.http}, code=${codeOf(postLoadedEvidence)}, requestId=${postLoadedEvidence.requestId || 'missing'}, traceId=${postLoadedEvidence.traceId || 'missing'}`, OPTIONS)
const postLoadedVoid = await request(context, 'POST',
`${base}/loading/evidence/${encodeURIComponent(id(fixture.immutableEvidenceId))}/void?tenantId=${encodeURIComponent(tenantId)}`,
{ expectedVersion: Number(fixture.immutableEvidenceVersion), reason: `${context.marker}-void-after-loaded` },
`/api/platform/v1/first-leg/pda-loading/tasks/${encodeURIComponent(id(fixture.canonicalPdaTaskId))}/evidence/${encodeURIComponent(id(fixture.immutableEvidenceId))}/void`,
{ warehouseId: id(fixture.warehouseId), expectedVersion: Number(fixture.immutableEvidenceVersion),
reason: `${context.marker}-void-after-loaded` },
{ 'Idempotency-Key': `${context.marker}-p3-evidence-void-after-loaded`.slice(0, 128) })
context.addStep(MODULE, 'LOADED 后作废装柜证据被拒绝', postLoadedVoid,
postLoadedVoid.http === 200 && codeOf(postLoadedVoid) === 'A0409'
@@ -361,7 +364,7 @@ export async function run(context) {
const immutableTransport = await request(context, 'PATCH',
`/api/platform/v1/first-leg/loading-plans/${encodeURIComponent(id(fixture.loadingPlanId))}/transport?warehouseId=${encodeURIComponent(id(fixture.warehouseId))}`,
{ containerNo: `MUTATE-${context.marker}`.slice(0, 64), voyageNo: 'MUTATE',
{ containerNo: fixture.containerNo, voyageNo: 'MUTATE',
expectedVersion: Number(immutablePlan?.version), operationReason: `${context.marker}-after-loaded` },
{ 'Idempotency-Key': `${context.marker}-p3-transport-after-loaded`.slice(0, 128) })
context.addStep(MODULE, 'LOADED 后运输信息修改被拒绝', immutableTransport,
@@ -212,13 +212,12 @@ export async function run(context) {
const inboundId = id(flow?.inboundId)
if (knownAllocation && inboundId) {
const selector = `inboundId=${encodeURIComponent(inboundId)}&containerBookingId=${encodeURIComponent(knownAllocation)}`
const expiredGateResponse = await request(context, 'GET', `${OUTBOUND}/check-gate?${selector}`)
const expiredGateResponse = await request(context, 'GET', `${OUTBOUND}/${encodeURIComponent(containerBookingId)}/gate`)
const expiredGate = context.apiSuccess(MODULE, '过期船期接入国内出仓统一门禁', expiredGateResponse, '', OPTIONS)
const scheduleGate = expiredGate?.items?.find?.((item) => item?.code === 'SAILING_SCHEDULE')
context.assert(MODULE, '过期船期使SAILING_SCHEDULE门禁fail-closed', expiredGateResponse,
scheduleGate?.passed === false && scheduleGate?.actual === 'EXPIRED',
`passed=${scheduleGate?.passed ?? 'missing'}, actual=${scheduleGate?.actual || 'missing'}`, OPTIONS)
const gateItems = (expiredGate?.inbounds || []).flatMap((item) => item?.gate?.items || [])
context.assert(MODULE, '船期独立于当前国内离仓六项门禁', expiredGateResponse,
Array.isArray(expiredGate?.inbounds) && !gateItems.some((item) => item?.code === 'SAILING_SCHEDULE'),
`inbounds=${expiredGate?.inbounds?.length ?? 'missing'}, obsoleteScheduleGate=${gateItems.some((item) => item?.code === 'SAILING_SCHEDULE')}`, OPTIONS)
}
const currentData = scheduleData(context, suffix, ocean.id, knownAllocation, master.customerId, false)
@@ -237,13 +236,12 @@ export async function run(context) {
`expired=${refreshed?.portSchedule?.expired ?? 'missing'}, sourceType=${refreshed?.portSchedule?.schedule?.sourceType || 'missing'}, version=${refreshed?.portSchedule?.schedule?.version ?? 'missing'}`, OPTIONS)
if (knownAllocation && inboundId) {
const selector = `inboundId=${encodeURIComponent(inboundId)}&containerBookingId=${encodeURIComponent(knownAllocation)}`
const readyGateResponse = await request(context, 'GET', `${OUTBOUND}/check-gate?${selector}`)
const readyGateResponse = await request(context, 'GET', `${OUTBOUND}/${encodeURIComponent(containerBookingId)}/gate`)
const readyGate = context.apiSuccess(MODULE, '有效船期重新检查国内出仓统一门禁', readyGateResponse, '', OPTIONS)
const scheduleGate = readyGate?.items?.find?.((item) => item?.code === 'SAILING_SCHEDULE')
context.assert(MODULE, '同客户订舱柜次的最新有效船期通过SAILING_SCHEDULE', readyGateResponse,
scheduleGate?.passed === true && String(scheduleGate?.actual || '').startsWith('READY:scheduleId='),
`passed=${scheduleGate?.passed ?? 'missing'}, actual=${scheduleGate?.actual || 'missing'}`, OPTIONS)
const gateItems = (readyGate?.inbounds || []).flatMap((item) => item?.gate?.items || [])
context.assert(MODULE, '有效船期不改变当前国内离仓六项门禁集合', readyGateResponse,
Array.isArray(readyGate?.inbounds) && !gateItems.some((item) => item?.code === 'SAILING_SCHEDULE'),
`inbounds=${readyGate?.inbounds?.length ?? 'missing'}, obsoleteScheduleGate=${gateItems.some((item) => item?.code === 'SAILING_SCHEDULE')}`, OPTIONS)
}
const forged = await request(context, 'POST', BASE, {
@@ -41,6 +41,12 @@ async function overseasMutation(context, method, name, path, body) {
context.addStep(MODULE, name, response, passed, trace(response), OPTIONS)
return { response, data: passed ? response.body?.data : null }
}
async function domesticMutation(context, method, name, path, body) {
const response = await context.request(method, path, body)
const passed = response?.http === 200 && String(response?.body?.code) === '200'
context.addStep(MODULE, name, response, passed, trace(response), OPTIONS)
return { response, data: passed ? response.body?.data : null }
}
async function overseasCsv(context, name, path) {
const response = await context.requestOverseasBinary('GET', path)
context.addStep(MODULE, name, response,
@@ -309,9 +315,9 @@ async function verifyRelabelLifecycle(context, customerId, warehouseId) {
async function createWarehouseFixture(context, customerId) {
const suffix = String(context.marker || '').replace(/[^A-Za-z0-9]/g, '').slice(-20).toUpperCase()
const payload = {
customerId, warehouseCode: `P4W-${suffix}`.slice(0, 50), warehouseName: `${context.marker} 海外仓`,
tenantId: 0, warehouseCode: `P4W-${suffix}`.slice(0, 50), warehouseName: `${context.marker} 海外仓`,
warehouseType: 'OVERSEAS', countryCode: 'US', timezone: 'America/Los_Angeles',
contactName: 'Phase4 QA', contactPhone: '+1-213-555-0188', contactEmail: null,
contactName: 'Phase4 QA', contactPhone: '12135550188', contactEmail: null,
status: 'ACTIVE', remark: `${context.marker}-warehouse-fixture`,
address: { addressType: 'WAREHOUSE', countryCode: 'US', stateProvince: 'California', city: 'Los Angeles',
district: 'Commerce', postalCode: '90040', addressLine1: '501 Phase Four Warehouse Avenue',
@@ -319,22 +325,22 @@ async function createWarehouseFixture(context, customerId) {
capabilities: [{ capabilityCode: 'STORAGE', capabilityName: 'Storage', enabled: true,
cutoffTime: null, description: `${context.marker} acceptance` }],
}
const created = await overseasMutation(context, 'POST', '创建本轮客户海外仓', '/api/platform/v1/warehouse-admin', payload)
const created = await domesticMutation(context, 'POST', '创建本轮客户海外仓', '/api/platform/v1/warehouse-admin', payload)
const warehouseId = id(created.data)
context.assert(MODULE, '客户海外仓创建返回长ID', created.response, longId(warehouseId),
`warehouseId=${warehouseId || 'missing'}, customerId=${customerId}; ${trace(created.response)}`, OPTIONS)
if (!warehouseId) return null
const detail = await overseasGet(context, '查询本轮客户海外仓详情', query(`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}`, { customerId }))
context.assert(MODULE, '本轮客户海外仓归属和状态正确', detail.response,
const detail = await domesticGet(context, '查询本轮公共海外仓详情', query(`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}`, { tenantId: 0 }))
context.assert(MODULE, '本轮公共海外仓类型和状态正确', detail.response,
id(detail.data?.warehouse?.id || detail.data?.id) === warehouseId
&& id(detail.data?.warehouse?.customerId || detail.data?.customerId) === customerId
&& (detail.data?.warehouse?.warehouseType || detail.data?.warehouseType) === 'OVERSEAS'
&& (detail.data?.warehouse?.status || detail.data?.status) === 'ACTIVE',
`warehouseId=${warehouseId}, customerId=${id(detail.data?.warehouse?.customerId || detail.data?.customerId) || 'missing'}; ${trace(detail.response)}`, OPTIONS)
`warehouseId=${warehouseId}, warehouseType=${detail.data?.warehouse?.warehouseType || detail.data?.warehouseType || 'missing'}; ${trace(detail.response)}`, OPTIONS)
return { id: warehouseId, customerId, warehouseCode: payload.warehouseCode, warehouseName: payload.warehouseName, status: 'ACTIVE' }
}
async function retireWarehouseFixture(context, customerId, warehouseId) {
const retired = await context.requestOverseas('PATCH', query(`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/status`, { customerId, status: 'INACTIVE' }))
const retired = await context.request('PATCH', query(`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/status`, { tenantId: 0, status: 'INACTIVE' }))
context.addStep(MODULE, '本轮客户海外仓已停用收口', retired,
retired?.http === 200 && String(retired?.body?.code) === '200', trace(retired), OPTIONS)
}
@@ -400,23 +406,21 @@ async function verifyPalletLifecycle(context, warehouseId) {
}
async function findWarehouse(context, tenants, preferredCustomerId) {
const candidates = [...new Set([preferredCustomerId, ...tenants.map((item) => id(item?.id)).filter(longId)])]
let lastResponse = null
for (const customerId of candidates) {
const result = await overseasGet(context, `查询客户${customerId}可用海外仓`, query('/api/platform/v1/warehouse-admin', { customerId, page: 1, size: 100, status: 'ACTIVE' }))
assertPage(context, `客户${customerId}可用海外仓`, result)
lastResponse = result.response
const warehouse = records(result.data).find((item) => longId(item?.id) && item?.status === 'ACTIVE')
if (warehouse) {
context.assert(MODULE, '海外仓关键字段正确', result.response,
nonBlank(warehouse.warehouseCode) && nonBlank(warehouse.warehouseName),
`warehouseId=${id(warehouse.id)}, code=${warehouse.warehouseCode || 'missing'}; ${trace(result.response)}`, OPTIONS)
const detail = await overseasGet(context, '查询海外仓详情', query(`/api/platform/v1/warehouse-admin/${encodeURIComponent(id(warehouse.id))}`, { customerId }))
context.assert(MODULE, '海外仓详情对应列表记录', detail.response,
id(detail.data?.warehouse?.id || detail.data?.id) === id(warehouse.id),
`listId=${id(warehouse.id)}, detailId=${id(detail.data?.warehouse?.id || detail.data?.id) || 'missing'}; ${trace(detail.response)}`, OPTIONS)
return warehouse
}
const result = await domesticGet(context, '查询可用公共海外仓', query('/api/platform/v1/warehouse-admin', {
tenantId: 0, warehouseType: 'OVERSEAS', page: 1, size: 100, status: 'ACTIVE',
}))
assertPage(context, '可用公共海外仓', result)
const warehouse = records(result.data).find((item) => longId(item?.id)
&& item?.status === 'ACTIVE' && item?.warehouseType === 'OVERSEAS')
if (warehouse) {
context.assert(MODULE, '海外仓关键字段正确', result.response,
nonBlank(warehouse.warehouseCode) && nonBlank(warehouse.warehouseName),
`warehouseId=${id(warehouse.id)}, code=${warehouse.warehouseCode || 'missing'}; ${trace(result.response)}`, OPTIONS)
const detail = await domesticGet(context, '查询海外仓详情', query(`/api/platform/v1/warehouse-admin/${encodeURIComponent(id(warehouse.id))}`, { tenantId: 0 }))
context.assert(MODULE, '海外仓详情对应列表记录', detail.response,
id(detail.data?.warehouse?.id || detail.data?.id) === id(warehouse.id),
`listId=${id(warehouse.id)}, detailId=${id(detail.data?.warehouse?.id || detail.data?.id) || 'missing'}; ${trace(detail.response)}`, OPTIONS)
return warehouse
}
return null
}
@@ -30,8 +30,8 @@ async function actionRequest(context, path, endpoint, version) {
}
}
async function uploadBillOfLading(context, appointmentId) {
const path = `${TRUCK}/${appointmentId}/attachments/upload?attachmentType=BILL_OF_LADING`
async function uploadDispatchOrder(context, appointmentId) {
const path = `${TRUCK}/${appointmentId}/attachments/upload?attachmentType=DISPATCH_ORDER`
const form = new FormData()
const png = Uint8Array.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,6,0,0,0,31,21,196,137,0,0,0,13,73,68,65,84,8,215,99,248,207,192,240,31,0,5,0,1,255,137,153,61,29,0,0,0,0,73,69,78,68,174,66,96,130])
form.append('file', new Blob([png], { type: 'image/png' }), `${context.marker}-bill.png`)
@@ -51,11 +51,6 @@ export async function run(context) {
const ocean = containerFlow?.oceanBookingId
? { id: id(containerFlow.oceanBookingId), finalStatus: 'RELEASED' }
: context.createdResources?.oceanBooking
const containerFixture = [
context.createdResources?.orders,
context.createdResources?.orchestrationFixtures?.inboundMeasurement,
context.createdResources?.orchestrationFixtures?.phase3Container,
].find((item) => id(item?.inboundId) === id(containerFlow?.inboundId))
if (!ocean?.id || ocean.finalStatus !== 'RELEASED') {
context.addStep(MODULE, '跨服务链路前置订舱终态可用', null, false,
'本轮海运订舱模块未返回 RELEASED 隔离记录,未伪造关联拖车预约', OPTIONS)
@@ -65,19 +60,10 @@ export async function run(context) {
const suffix = context.marker.replace(/\D/g, '').slice(-10)
const oceanResponse = await context.request('GET', `${OCEAN}/${ocean.id}`)
const oceanDetail = context.apiSuccess(MODULE, '读取跨服务链路已放舱订舱', oceanResponse, `oceanBookingId=${ocean.id}`, OPTIONS)
const linkContainer = ['AWAITING_REVIEW', 'LOADED'].includes(containerFlow?.finalStatus)
&& Boolean(containerFlow?.containerBookingId)
&& Boolean(containerFlow?.loadingPlanId)
if (!linkContainer) {
context.addStep(MODULE, '本轮已放舱订舱必须关联真实柜和装柜计划', oceanResponse, false,
`containerStatus=${containerFlow?.finalStatus || 'missing'}, containerBookingId=${id(containerFlow?.containerBookingId) || 'missing'}`, OPTIONS)
return
}
const allocationMatches = oceanDetail?.containers?.some?.((item) =>
id(item?.containerBookingId) === id(containerFlow.containerBookingId))
context.assert(MODULE, '已放舱订舱保留本轮柜次分配', oceanResponse, allocationMatches,
`containerBookingId=${id(containerFlow.containerBookingId)}, containers=${oceanDetail?.containers?.length ?? 'n/a'}`, OPTIONS)
if (!allocationMatches) return
const booking = oceanDetail?.oceanBooking || oceanDetail
context.assert(MODULE, '已放舱订舱可直接创建订舱级拖车预约', oceanResponse,
id(booking?.id) === id(ocean.id) && booking?.status === 'RELEASED',
`oceanBookingId=${id(booking?.id) || 'missing'}, status=${booking?.status || 'missing'}`, OPTIONS)
const carrierResponse = await context.request('GET',
'/api/platform/v1/suppliers?tenantId=0&category=TRUCKING&status=ACTIVE&page=1&size=20')
const carrierPage = context.apiSuccess(MODULE, '读取 ACTIVE TRUCKING 承运商事实源', carrierResponse, '', OPTIONS)
@@ -86,13 +72,12 @@ export async function run(context) {
Boolean(carrier?.id && carrier?.name), `carrierId=${id(carrier?.id) || 'missing'}`, OPTIONS)
if (!carrier) return
const payload = {
outboundOrderId: linkContainer ? id(containerFixture?.orderId) || null : null,
outboundOrderId: null,
oceanBookingId: ocean.id,
containerBookingId: linkContainer ? id(containerFlow.containerBookingId) : null,
loadingPlanId: linkContainer ? id(containerFlow.loadingPlanId) : null,
warehouseId: linkContainer
? id(containerFlow.warehouseId)
: id(context.createdResources?.warehouse?.id || context.context?.warehouseId || '193000000000000001'),
containerBookingId: null,
loadingPlanId: null,
warehouseId: id(containerFlow?.warehouseId
|| context.createdResources?.warehouse?.id || context.context?.warehouseId || '193000000000000001'),
carrierId: id(carrier.id), carrierName: carrier.name,
fleetContact: '链路车队联系人', fleetPhone: '13800000007', driverName: '链路司机', driverPhone: '13800000008',
driverIdCard: null, vehicleNo: `粤D-${suffix.slice(-6)}`, storeName: `链路门店-${suffix}`, storeContact: '链路联系人',
@@ -101,28 +86,19 @@ export async function run(context) {
plannedLoadingAt: '2026-09-03T08:00:00', transitMinutes: 60, payableAmount: 1,
currency: 'RMB', remark: context.marker, expectedVersion: 0,
}
if (linkContainer) {
const missingContainerResponse = await context.request('POST', TRUCK, {
...payload, containerBookingId: null, remark: `${context.marker}-missing-container`,
})
context.addStep(MODULE, '拖车关联装柜计划时缺失柜次被拒绝', missingContainerResponse,
missingContainerResponse.http === 200 && codeOf(missingContainerResponse) !== '200',
`HTTP=${missingContainerResponse.http}, code=${codeOf(missingContainerResponse)}`, OPTIONS)
}
const response = await context.request('POST', TRUCK, payload)
const appointment = context.apiSuccess(MODULE, '创建关联海运订舱的拖车预约', response, `oceanBookingId=${ocean.id}`, OPTIONS)
const appointmentId = id(appointment?.id)
context.assert(MODULE, '拖车预约关联订舱、柜次、装柜计划、版本和审计字段完整', response,
context.assert(MODULE, '拖车预约仅关联订舱且版本和审计字段完整', response,
Boolean(appointmentId) && id(appointment?.oceanBookingId) === id(ocean.id) && Number.isFinite(Number(appointment?.version))
&& (!linkContainer || id(appointment?.containerBookingId) === id(containerFlow.containerBookingId)
&& id(appointment?.loadingPlanId) === id(containerFlow.loadingPlanId))
&& !id(appointment?.containerBookingId) && !id(appointment?.loadingPlanId)
&& Boolean(appointment?.createdAt || appointment?.createdBy || appointment?.operatorName),
`appointmentId=${appointmentId || 'missing'}, oceanBookingId=${appointment?.oceanBookingId || 'missing'}, containerBookingId=${appointment?.containerBookingId || 'n/a'}, loadingPlanId=${appointment?.loadingPlanId || 'n/a'}, version=${appointment?.version ?? 'missing'}`, OPTIONS)
if (!appointmentId) return
const path = `${TRUCK}/${appointmentId}`
const billUpload = await uploadBillOfLading(context, appointmentId)
const billAttachment = context.apiSuccess(MODULE, '跨服务拖车上传有效提单附件', billUpload, '', OPTIONS)
if (!billAttachment?.id) return
const dispatchUpload = await uploadDispatchOrder(context, appointmentId)
const dispatchAttachment = context.apiSuccess(MODULE, '跨服务拖车上传有效派车单附件', dispatchUpload, '', OPTIONS)
if (!dispatchAttachment?.id) return
let version = Number(appointment.version)
for (const [name, endpoint, status] of [
['提交跨链路预约', 'submit', 'PENDING_DISPATCH'],
@@ -144,8 +120,7 @@ export async function run(context) {
const final = context.apiSuccess(MODULE, '回查跨服务链路安全终态', detail, '', OPTIONS)
context.assert(MODULE, '跨服务链路终态持久化且关联订舱未漂移', detail,
final?.status === 'COMPLETED' && id(final?.oceanBookingId) === id(ocean.id)
&& (!linkContainer || id(final?.containerBookingId) === id(containerFlow.containerBookingId)
&& id(final?.loadingPlanId) === id(containerFlow.loadingPlanId)),
&& !id(final?.containerBookingId) && !id(final?.loadingPlanId),
`status=${final?.status || 'missing'}, oceanBookingId=${final?.oceanBookingId || 'missing'}, containerBookingId=${final?.containerBookingId || 'n/a'}, loadingPlanId=${final?.loadingPlanId || 'n/a'}`, OPTIONS)
const historyResponse = await context.request('GET', `${TRUCK}/${appointmentId}/history`)
@@ -161,8 +136,8 @@ export async function run(context) {
if (final?.status === 'COMPLETED') {
context.createdResources.serviceCenterFlow = {
appointmentId, oceanBookingId: ocean.id,
containerBookingId: linkContainer ? id(containerFlow.containerBookingId) : null,
loadingPlanId: linkContainer ? id(containerFlow.loadingPlanId) : null,
containerBookingId: null,
loadingPlanId: null,
marker: context.marker, finalStatus: 'COMPLETED',
}
}