Files
baiyue-firstleg/scripts/admin-test-env-modules/phase3-customs.mjs
T

169 lines
9.5 KiB
JavaScript

const MODULE = 'phase3-customs'
const OPTIONS = { scenario: 'phase3-container-customs-producer', deploymentBlocker: true }
const PLATFORM_CUSTOMS = '/api/platform/v1/customs/loading-plans'
function id(value) { return value == null ? '' : String(value) }
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
function key(context, operation) { return `${context.marker}-container-customs-${operation}`.slice(0, 128) }
async function request(context, method, path, body, headers = {}) {
try {
const response = await fetch(`${context.options.gateway}${path}`, {
method,
headers: {
Accept: 'application/json', Authorization: context.token ? `Bearer ${context.token}` : '',
...(body === undefined || body instanceof FormData ? {} : { 'Content-Type': 'application/json; charset=utf-8' }),
...headers,
},
body: body === undefined ? undefined : body instanceof FormData ? body : JSON.stringify(body),
signal: AbortSignal.timeout(60_000),
})
const raw = await response.text(); let payload = null
try { payload = raw ? JSON.parse(raw) : null } catch { /* bounded raw evidence */ }
return {
method, path, http: response.status, body: payload, raw: payload === null ? raw.slice(0, 500) : null,
requestId: payload?.requestId || response.headers.get('x-request-id'),
traceId: payload?.traceId || response.headers.get('x-trace-id'),
}
} catch (error) {
return { method, path, http: 0, body: null, raw: String(error), requestId: null, traceId: null }
}
}
async function download(context, path) {
try {
const response = await fetch(`${context.options.gateway}${path}`, {
headers: { Accept: '*/*', Authorization: `Bearer ${context.token}` }, signal: AbortSignal.timeout(60_000),
})
const bytes = new Uint8Array(await response.arrayBuffer())
return { method: 'GET', path, http: response.status, bytes,
contentType: response.headers.get('content-type') || '',
requestId: response.headers.get('x-request-id'), traceId: response.headers.get('x-trace-id') }
} catch (error) {
return { method: 'GET', path, http: 0, bytes: new Uint8Array(), contentType: '', raw: String(error) }
}
}
export function selectCustomsFlow(createdResources) {
const fixtures = [
createdResources?.orders,
createdResources?.orchestrationFixtures?.inboundMeasurement,
createdResources?.orchestrationFixtures?.phase3Container,
].filter(Boolean)
const containers = [
createdResources?.phase3Loading,
createdResources?.phase3ContainerLoading,
].filter(Boolean)
for (const container of containers) {
const fixture = fixtures.find((candidate) => id(candidate?.tenantId) === id(container?.tenantId)
&& id(candidate?.inboundId) === id(container?.inboundId))
if (fixture) return { fixture, container }
}
return { fixture: null, container: null }
}
async function ensureReleasedOceanBooking(context, container) {
const containerBookingId = id(container?.containerBookingId)
const oceanBookingId = id(container?.oceanBookingId)
if (!oceanBookingId) {
context.addStep(MODULE, '报关前订舱已放舱并绑定实际装柜柜次', null, false,
`oceanBookingId=missing, containerBookingId=${containerBookingId || 'missing'}`, OPTIONS)
return false
}
const response = await context.request('GET', `/api/platform/v1/ocean-bookings/${encodeURIComponent(oceanBookingId)}`)
const detail = context.apiSuccess(MODULE, '报关前回读装柜链正式订舱', response, '', OPTIONS)
const allocated = detail?.containers?.some?.((item) => id(item?.containerBookingId) === containerBookingId)
const passed = detail?.oceanBooking?.status === 'RELEASED' && allocated
context.addStep(MODULE, '报关前订舱已放舱并绑定实际装柜柜次', response, passed,
`oceanStatus=${detail?.oceanBooking?.status || 'missing'}, oceanBookingId=${oceanBookingId}, containerBookingId=${containerBookingId}, allocated=${allocated === true}`, OPTIONS)
if (passed) context.createdResources.oceanBooking = {
id: oceanBookingId, marker: context.marker, finalStatus: 'RELEASED', containerBookingId,
loadingPlanId: id(container?.loadingPlanId),
}
return passed
}
async function verifyContainerOperationalDocuments(context, containerBookingId) {
const overviewResponse = await context.request('GET', `/api/platform/v1/containers/${containerBookingId}/overview`)
const overview = context.apiSuccess(MODULE, '只读复核前置模块已完成的整柜作业资料', overviewResponse, '', OPTIONS)
const documents = Array.isArray(overview?.documents) ? overview.documents : []
const photos = Array.isArray(overview?.pda?.photos) ? overview.pda.photos.filter((item) => item?.status === 'ACTIVE') : []
const passed = ['DESTINATION_CLEARANCE', 'DELIVERY_ORDER', 'PICKUP_ORDER']
.every((type) => documents.some((item) => item?.documentType === type))
&& documents.some((item) => item?.documentType === 'DESTINATION_CLEARANCE' && item?.completed === true)
&& photos.length >= 5
context.assert(MODULE, '整柜聚合回显三类当前作业资料与真实PDA照片证据', overviewResponse, passed,
`documents=${documents.map((item) => item?.documentType).join('|') || 'none'}, activePhotos=${photos.length}`, OPTIONS)
for (const [index, photo] of photos.entries()) {
const response = await download(context, photo.downloadPath)
context.assert(MODULE, `下载整柜PDA照片证据${index + 1}`, response,
response.http === 200 && response.bytes.length > 0 && !response.contentType.includes('application/json'),
`fileId=${id(photo?.fileId) || 'missing'}, bytes=${response.bytes.length}`, OPTIONS)
}
return passed
}
async function completeContainerCustoms(context, loadingPlanId) {
const base = `${PLATFORM_CUSTOMS}/${encodeURIComponent(loadingPlanId)}`
let response = await request(context, 'GET', base)
let detail = context.apiSuccess(MODULE, '读取正式整柜报关聚合', response,
`loadingPlanId=${loadingPlanId}`, OPTIONS)
if (!detail?.aggregateId) return null
if (detail.status === 'COMPLETED') return detail
if (detail.status === 'READY_TO_START') {
response = await request(context, 'POST', `${base}/start`, {
expectedVersion: Number(detail.version), confirm: true, remark: context.marker,
}, { 'Idempotency-Key': key(context, 'start') })
detail = context.apiSuccess(MODULE, '正式整柜报关开始并冻结资料', response, '', OPTIONS)
context.assert(MODULE, '正式整柜报关进入DECLARING', response,
detail?.status === 'DECLARING', `status=${detail?.status || 'missing'}`, OPTIONS)
}
const allBuy = Array.isArray(detail?.inbounds) && detail.inbounds.length > 0
&& detail.inbounds.every((item) => item?.taxMode === 'BUY_DECLARATION')
context.assert(MODULE, '本轮生产者整柜为全买单正式完成路径', response, allBuy,
`taxModes=${(detail?.inbounds || []).map((item) => item?.taxMode).join('|') || 'missing'}`, OPTIONS)
if (!allBuy || detail?.status !== 'DECLARING') return null
response = await request(context, 'POST', `${base}/complete-all-buy`, {
expectedVersion: Number(detail.version), confirm: true, remark: context.marker,
}, { 'Idempotency-Key': key(context, 'complete-all-buy') })
const completed = context.apiSuccess(MODULE, '正式全买单整柜报关完成', response, '', OPTIONS)
context.assert(MODULE, '正式整柜报关完成并满足出仓报关门禁', response,
completed?.status === 'COMPLETED' && Boolean(completed?.completedAt),
`status=${completed?.status || 'missing'}, completedAt=${completed?.completedAt || 'missing'}`, OPTIONS)
return completed?.status === 'COMPLETED' ? completed : null
}
export async function run(context) {
const { fixture, container } = selectCustomsFlow(context.createdResources)
const tenantId = id(fixture?.tenantId)
const inboundId = id(fixture?.inboundId)
const containerBookingId = id(container?.containerBookingId)
const loadingPlanId = id(container?.loadingPlanId)
if (!tenantId || !inboundId || !containerBookingId || !loadingPlanId
|| !['AWAITING_REVIEW', 'LOADED'].includes(container?.finalStatus)
|| (container?.finalStatus === 'AWAITING_REVIEW'
&& (container?.loadingPlanStatus !== 'LOADING' || !container?.canonicalPdaTaskId))) {
context.addStep(MODULE, '取得本轮客户、入仓单与柜次隔离fixture', null, false,
`tenantId=${tenantId || 'missing'}, inboundId=${inboundId || 'missing'}, containerBookingId=${containerBookingId || 'missing'}, loadingPlanId=${loadingPlanId || 'missing'}, containerStatus=${container?.finalStatus || 'missing'}`,
OPTIONS)
return
}
if (!await ensureReleasedOceanBooking(context, container)) return
if (!await verifyContainerOperationalDocuments(context, containerBookingId)) {
context.addStep(MODULE, '三类整柜作业资料必须完成后才允许进入出仓确认', null, false,
`containerBookingId=${containerBookingId}, loadingPlanId=${loadingPlanId}`, OPTIONS)
return
}
const customs = await completeContainerCustoms(context, loadingPlanId)
if (!customs) {
context.addStep(MODULE, '正式整柜报关聚合必须完成后才允许进入出仓确认', null, false,
`containerBookingId=${containerBookingId}, loadingPlanId=${loadingPlanId}`, OPTIONS)
return
}
context.createdResources.phase3Customs = {
marker: context.marker, tenantId, orderId: id(fixture?.orderId), inboundId,
containerBookingId, loadingPlanId, customsContainerId: id(customs.aggregateId),
finalStatus: 'COMPLETED',
}
}