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', } }