test(acceptance): align current gateway contracts
This commit is contained in:
@@ -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',
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user