2026-08-17 19:51:54 +08:00
|
|
|
import { customsCaseFieldGaps } from './fulfillment-list-field-contract.mjs'
|
|
|
|
|
|
2026-08-08 22:42:49 +08:00
|
|
|
const MODULE = 'phase3-customs'
|
|
|
|
|
const OPTIONS = { scenario: 'phase3-customs-collaboration', deploymentBlocker: true }
|
|
|
|
|
const PLATFORM = '/api/platform/v1/customs'
|
|
|
|
|
const MERCHANT = '/api/merchant/v1/customs'
|
2026-08-26 11:10:42 +08:00
|
|
|
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
|
|
|
|
|
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
|
2026-08-08 22:42:49 +08:00
|
|
|
|
|
|
|
|
function id(value) { return value == null ? '' : String(value) }
|
|
|
|
|
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
|
|
|
|
|
function failure(response) {
|
|
|
|
|
return [400, 401, 403, 404, 409, 503].includes(response?.http)
|
|
|
|
|
|| (response?.http === 200 && codeOf(response) !== '200')
|
|
|
|
|
}
|
|
|
|
|
function key(context, operation) { return `${context.marker}-customs-${operation}`.slice(0, 128) }
|
|
|
|
|
|
|
|
|
|
async function request(context, method, path, body, token = context.token, headers = {}) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${context.options.gateway}${path}`, {
|
|
|
|
|
method,
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: 'application/json', Authorization: token ? `Bearer ${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 upload(context, path, fields, content, fileName, token, idempotencyKey) {
|
|
|
|
|
const form = new FormData()
|
|
|
|
|
for (const [name, value] of Object.entries(fields)) if (value !== null && value !== undefined) form.append(name, String(value))
|
|
|
|
|
form.append('file', new Blob([content], { type: 'text/plain' }), fileName)
|
|
|
|
|
return request(context, 'POST', path, form, token, { 'Idempotency-Key': idempotencyKey })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function download(context, path, token) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${context.options.gateway}${path}`, {
|
|
|
|
|
headers: { Accept: '*/*', Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(60_000),
|
|
|
|
|
})
|
|
|
|
|
const bytes = new Uint8Array(await response.arrayBuffer())
|
|
|
|
|
let body = null
|
|
|
|
|
const contentType = response.headers.get('content-type') || ''
|
|
|
|
|
if (contentType.includes('application/json')) {
|
|
|
|
|
try { body = JSON.parse(new TextDecoder().decode(bytes)) } catch { /* binary evidence remains bounded */ }
|
|
|
|
|
}
|
|
|
|
|
return { method: 'GET', path, http: response.status, bytes,
|
|
|
|
|
body, contentType, requestId: body?.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) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function detail(context, caseId) {
|
|
|
|
|
const response = await context.request('GET', `${PLATFORM}/cases/${caseId}`)
|
|
|
|
|
const data = context.apiSuccess(MODULE, '回查报关协同聚合与版本', response, `caseId=${caseId}`, OPTIONS)
|
|
|
|
|
return { response, data }
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-11 14:07:16 +08:00
|
|
|
export function selectCustomsFlow(createdResources) {
|
|
|
|
|
const fixtures = [
|
2026-08-11 18:03:44 +08:00
|
|
|
createdResources?.orders,
|
2026-08-11 14:07:16 +08:00
|
|
|
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 }
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 15:55:11 +08:00
|
|
|
async function ensureReleasedOceanBooking(context, container) {
|
2026-08-11 18:03:44 +08:00
|
|
|
const containerBookingId = id(container?.containerBookingId)
|
|
|
|
|
const loadingPlanId = id(container?.loadingPlanId)
|
2026-08-24 11:48:16 +08:00
|
|
|
const oceanBookingId = id(container?.oceanBookingId)
|
|
|
|
|
if (!oceanBookingId) {
|
2026-08-11 18:03:44 +08:00
|
|
|
context.addStep(MODULE, '报关前订舱已放舱并绑定实际装柜柜次', null, false,
|
2026-08-24 11:48:16 +08:00
|
|
|
`oceanBookingId=missing, containerBookingId=${containerBookingId || 'missing'}`, OPTIONS)
|
2026-08-11 18:03:44 +08:00
|
|
|
return false
|
|
|
|
|
}
|
2026-08-24 11:48:16 +08:00
|
|
|
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)
|
|
|
|
|
if (detail?.oceanBooking?.status !== 'RELEASED' || !allocated) {
|
|
|
|
|
context.addStep(MODULE, '报关前订舱已放舱并绑定实际装柜柜次', response, false,
|
|
|
|
|
`oceanStatus=${detail?.oceanBooking?.status || 'missing'}, oceanBookingId=${oceanBookingId}, containerBookingId=${containerBookingId || 'missing'}, allocated=${allocated === true}`, OPTIONS)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
context.createdResources.oceanBooking = {
|
|
|
|
|
id: oceanBookingId, marker: context.marker, finalStatus: 'RELEASED',
|
|
|
|
|
containerBookingId, loadingPlanId,
|
|
|
|
|
}
|
|
|
|
|
context.addStep(MODULE, '报关前订舱已放舱并绑定实际装柜柜次', response, true,
|
|
|
|
|
`oceanBookingId=${oceanBookingId}, containerBookingId=${containerBookingId}`, OPTIONS)
|
2026-08-17 15:55:11 +08:00
|
|
|
return true
|
2026-08-11 18:03:44 +08:00
|
|
|
}
|
|
|
|
|
|
2026-08-26 14:56:33 +08:00
|
|
|
async function uploadVersion(context, path, label, expectedVersion, operation, content) {
|
|
|
|
|
const response = await upload(context, path, {
|
|
|
|
|
expectedVersion, operationReason: `${context.marker}-${operation}`,
|
|
|
|
|
}, content, `${context.marker}-${operation}.txt`, context.token, key(context, operation))
|
|
|
|
|
const document = context.apiSuccess(MODULE, label, response, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, `${label}返回当前唯一文件事实`, response,
|
|
|
|
|
Boolean(document?.id) && Boolean(document?.fileId) && Number.isInteger(Number(document?.version))
|
|
|
|
|
&& /^[0-9a-f]{64}$/i.test(String(document?.fileHash || '')),
|
|
|
|
|
`documentId=${id(document?.id) || 'missing'}, fileId=${id(document?.fileId) || 'missing'}, version=${document?.version ?? 'missing'}`, OPTIONS)
|
|
|
|
|
return { response, document }
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-09 02:54:09 +08:00
|
|
|
async function verifyDomesticCustomsEvidence(context, { containerBookingId, loadingPlanId, warehouseId }) {
|
2026-08-26 14:56:33 +08:00
|
|
|
const reason = `${context.marker}-人工验收节点资料`
|
2026-09-09 02:54:09 +08:00
|
|
|
const type = 'CUSTOMS_DECLARATION'
|
|
|
|
|
const query = `warehouseId=${encodeURIComponent(warehouseId)}`
|
|
|
|
|
const base = `/api/platform/v1/first-leg/loading-lists/${loadingPlanId}/customs`
|
|
|
|
|
const uploadPath = `${base}/document/upload?${query}`
|
|
|
|
|
const first = await uploadVersion(context, uploadPath, `首次上传${type}`, 0,
|
|
|
|
|
`${type.toLowerCase()}-v1`, `${context.marker} ${type} v1`)
|
|
|
|
|
if (!first.document?.fileId) return false
|
|
|
|
|
const firstRead = await context.request('GET', `${base}?${query}`)
|
|
|
|
|
const firstEcho = context.apiSuccess(MODULE, `回显${type}首版`, firstRead, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, `${type}首版回显身份一致`, firstRead,
|
|
|
|
|
id(firstEcho?.id) === id(first.document.id) && id(firstEcho?.fileId) === id(first.document.fileId),
|
|
|
|
|
`documentId=${id(firstEcho?.id) || 'missing'}, version=${firstEcho?.version ?? 'missing'}`, OPTIONS)
|
|
|
|
|
const firstDownload = await download(context,
|
|
|
|
|
`${base}/document/${id(first.document.fileId)}/download?${query}`, context.token)
|
|
|
|
|
context.assert(MODULE, `下载${type}首版`, firstDownload,
|
|
|
|
|
firstDownload.http === 200 && firstDownload.bytes.length > 0 && firstDownload.contentType.includes('text/plain'),
|
|
|
|
|
`HTTP=${firstDownload.http}, bytes=${firstDownload.bytes.length}`, OPTIONS)
|
|
|
|
|
const second = await uploadVersion(context, uploadPath, `更新${type}为第二版`,
|
|
|
|
|
Number(firstEcho?.version), `${type.toLowerCase()}-v2`, `${context.marker} ${type} v2`)
|
|
|
|
|
context.assert(MODULE, `${type}更新保持同一聚合并递增版本`, second.response,
|
|
|
|
|
id(second.document?.id) === id(first.document.id) && Number(second.document?.version) === Number(firstEcho?.version) + 1,
|
|
|
|
|
`documentId=${id(second.document?.id) || 'missing'}, version=${second.document?.version ?? 'missing'}`, OPTIONS)
|
|
|
|
|
const completeResponse = await request(context, 'POST', `${base}/complete?${query}`, {
|
|
|
|
|
expectedVersion: Number(second.document?.version), operationReason: reason,
|
|
|
|
|
}, context.token, { 'Idempotency-Key': key(context, `${type.toLowerCase()}-complete`) })
|
|
|
|
|
const complete = context.apiSuccess(MODULE, `完成${type}`, completeResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, `${type}完成态、时间和版本可回读`, completeResponse,
|
|
|
|
|
id(complete?.id) === id(first.document.id) && Boolean(complete?.completedAt)
|
|
|
|
|
&& Number(complete?.version) === Number(second.document?.version) + 1,
|
|
|
|
|
`completedAt=${complete?.completedAt || 'missing'}, version=${complete?.version ?? 'missing'}`, OPTIONS)
|
2026-08-26 14:56:33 +08:00
|
|
|
|
|
|
|
|
const overviewResponse = await context.request('GET', `/api/platform/v1/containers/${containerBookingId}/overview`)
|
2026-09-09 02:54:09 +08:00
|
|
|
const overview = context.apiSuccess(MODULE, '国内报关完成后回查整柜聚合', overviewResponse, '', OPTIONS)
|
2026-08-26 14:56:33 +08:00
|
|
|
const documents = Array.isArray(overview?.documents) ? overview.documents : []
|
|
|
|
|
const photos = Array.isArray(overview?.pda?.photos) ? overview.pda.photos.filter((item) => item?.status === 'ACTIVE') : []
|
2026-09-09 02:54:09 +08:00
|
|
|
context.assert(MODULE, '整柜聚合回显国内报关资料与真实PDA照片证据', overviewResponse,
|
|
|
|
|
documents.some((item) => item?.documentType === type
|
|
|
|
|
&& id(item?.id) === id(complete?.id) && item?.completed === true)
|
2026-08-26 14:56:33 +08:00
|
|
|
&& photos.length >= 5 && photos.every((item) => /^\/api\/platform\/v1\/containers\/\d+\/pda\/photos\/\d+\/download$/.test(String(item?.downloadPath || ''))),
|
|
|
|
|
`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.token)
|
|
|
|
|
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 true
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-08 22:42:49 +08:00
|
|
|
export async function run(context) {
|
2026-08-11 14:07:16 +08:00
|
|
|
const { fixture, container } = selectCustomsFlow(context.createdResources)
|
2026-08-08 22:42:49 +08:00
|
|
|
const tenantId = id(fixture?.tenantId)
|
2026-08-10 17:42:17 +08:00
|
|
|
const parentOrderId = id(fixture?.orderId)
|
2026-08-08 22:42:49 +08:00
|
|
|
const inboundId = id(fixture?.inboundId)
|
|
|
|
|
const containerBookingId = id(container?.containerBookingId)
|
2026-08-10 17:42:17 +08:00
|
|
|
const loadingPlanId = id(container?.loadingPlanId)
|
2026-08-26 14:56:33 +08:00
|
|
|
const warehouseId = id(container?.warehouseId || fixture?.warehouseId)
|
|
|
|
|
if (!tenantId || !parentOrderId || !inboundId || !containerBookingId || !loadingPlanId || !warehouseId
|
2026-08-17 15:41:42 +08:00
|
|
|
|| !['AWAITING_REVIEW', 'LOADED'].includes(container?.finalStatus)
|
|
|
|
|
|| (container?.finalStatus === 'AWAITING_REVIEW'
|
|
|
|
|
&& (container?.loadingPlanStatus !== 'LOADING' || !container?.canonicalPdaTaskId))) {
|
2026-08-08 22:42:49 +08:00
|
|
|
context.addStep(MODULE, '取得本轮客户/订单/入库单/柜次隔离 fixture', null, false,
|
2026-08-10 17:42:17 +08:00
|
|
|
`tenantId=${tenantId || 'missing'}, parentOrderId=${parentOrderId || 'missing'}, inboundId=${inboundId || 'missing'}, containerBookingId=${containerBookingId || 'missing'}, loadingPlanId=${loadingPlanId || 'missing'}, containerStatus=${container?.finalStatus || 'missing'}`, OPTIONS)
|
2026-08-08 22:42:49 +08:00
|
|
|
return
|
|
|
|
|
}
|
2026-08-17 15:55:11 +08:00
|
|
|
if (!await ensureReleasedOceanBooking(context, container)) return
|
2026-08-08 22:42:49 +08:00
|
|
|
|
2026-09-02 13:26:24 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-10 17:42:17 +08:00
|
|
|
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,
|
|
|
|
|
`containerBookingId=${containerBookingId}, tenantId=${tenantId}`, OPTIONS)
|
2026-08-11 11:41:41 +08:00
|
|
|
const carryingSlices = Array.isArray(slices)
|
2026-08-10 17:42:17 +08:00
|
|
|
? slices.filter((item) => id(item?.tenantId) === tenantId
|
|
|
|
|
&& id(item?.containerBookingId) === containerBookingId
|
|
|
|
|
&& id(item?.parentOrderId) === parentOrderId
|
|
|
|
|
&& item?.splitRole === 'LOADED'
|
|
|
|
|
&& (!loadingPlanId || id(item?.loadingPlanId) === loadingPlanId))
|
|
|
|
|
: []
|
2026-08-11 11:41:41 +08:00
|
|
|
const carryingSlice = carryingSlices.length === 1 ? carryingSlices[0] : null
|
|
|
|
|
const actualOrderId = id(carryingSlice?.childOrderId)
|
|
|
|
|
const sourceOrderId = id(carryingSlice?.sourceOrderId)
|
|
|
|
|
const sourceCarriesLoaded = Boolean(actualOrderId) && actualOrderId === sourceOrderId
|
2026-08-11 11:48:43 +08:00
|
|
|
const legacyLoadedChild = Boolean(actualOrderId) && Boolean(sourceOrderId)
|
|
|
|
|
&& actualOrderId !== sourceOrderId && actualOrderId !== parentOrderId
|
2026-08-11 11:41:41 +08:00
|
|
|
context.assert(MODULE, '报关绑定柜次实际承载的唯一 LOADED 来源分片', slicesResponse,
|
2026-08-11 11:48:43 +08:00
|
|
|
carryingSlices.length === 1 && (sourceCarriesLoaded || legacyLoadedChild)
|
2026-08-11 11:41:41 +08:00
|
|
|
&& id(carryingSlice?.parentOrderId) === parentOrderId
|
|
|
|
|
&& id(carryingSlice?.containerBookingId) === containerBookingId
|
|
|
|
|
&& id(carryingSlice?.tenantId) === tenantId
|
|
|
|
|
&& Number.isInteger(Number(carryingSlice?.packageCount)) && Number(carryingSlice.packageCount) > 0
|
|
|
|
|
&& (!loadingPlanId || id(carryingSlice?.loadingPlanId) === loadingPlanId),
|
2026-08-11 11:48:43 +08:00
|
|
|
`parentOrderId=${parentOrderId}, sourceOrderId=${sourceOrderId || 'missing'}, actualOrderId=${actualOrderId || 'missing'}, packageCount=${carryingSlice?.packageCount ?? 'missing'}, model=${sourceCarriesLoaded ? 'SOURCE_CARRIES_LOADED' : legacyLoadedChild ? 'LEGACY_LOADED_CHILD' : 'INVALID'}, containerBookingId=${containerBookingId}, loadingPlanId=${id(carryingSlice?.loadingPlanId) || 'missing'}, matches=${carryingSlices.length}`, OPTIONS)
|
|
|
|
|
if (carryingSlices.length !== 1 || (!sourceCarriesLoaded && !legacyLoadedChild)
|
2026-08-11 11:41:41 +08:00
|
|
|
|| !Number.isInteger(Number(carryingSlice?.packageCount)) || Number(carryingSlice.packageCount) <= 0) return
|
2026-08-10 17:42:17 +08:00
|
|
|
|
2026-08-08 22:42:49 +08:00
|
|
|
const loginResponse = await request(context, 'POST', '/api/auth/login', {
|
2026-09-03 18:13:00 +08:00
|
|
|
loginId: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
|
2026-08-08 22:42:49 +08:00
|
|
|
}, '')
|
|
|
|
|
const merchantLogin = context.apiSuccess(MODULE, '客户账号登录用于真实报关文件协同', loginResponse,
|
|
|
|
|
`username=${MERCHANT_USERNAME}`, OPTIONS)
|
|
|
|
|
const merchantToken = merchantLogin?.accessToken
|
|
|
|
|
if (!merchantToken) return
|
|
|
|
|
|
|
|
|
|
const suffix = context.marker.replace(/[^A-Za-z0-9]/g, '').slice(-18).toUpperCase()
|
|
|
|
|
const checklistPayload = {
|
|
|
|
|
checklistCode: `USFBA${suffix}`.slice(0, 40), versionNo: `V${suffix}`.slice(0, 32),
|
|
|
|
|
countryCode: 'US', channelCode: 'FBA', cargoType: 'GENERAL',
|
|
|
|
|
items: [{ code: 'INVOICE', name: '商业发票', required: true }],
|
|
|
|
|
}
|
|
|
|
|
const checklistResponse = await request(context, 'POST', `${PLATFORM}/checklist-versions`, checklistPayload,
|
|
|
|
|
context.token, { 'Idempotency-Key': key(context, 'checklist') })
|
|
|
|
|
const checklist = context.apiSuccess(MODULE, '创建本轮不可变报关资料清单版本', checklistResponse, '', OPTIONS)
|
|
|
|
|
const checklistVersionId = id(checklist?.version?.id)
|
|
|
|
|
context.assert(MODULE, '资料清单保留国家、渠道、货物类型和必填项目版本', checklistResponse,
|
|
|
|
|
Boolean(checklistVersionId) && checklist?.version?.countryCode === 'US' && checklist?.version?.channelCode === 'FBA'
|
|
|
|
|
&& checklist?.version?.cargoType === 'GENERAL' && checklist?.items?.[0]?.code === 'INVOICE'
|
|
|
|
|
&& checklist?.items?.[0]?.required === true,
|
|
|
|
|
`checklistVersionId=${checklistVersionId || 'missing'}, items=${checklist?.items?.length ?? 'n/a'}`, OPTIONS)
|
|
|
|
|
if (!checklistVersionId) return
|
|
|
|
|
|
2026-08-11 11:41:41 +08:00
|
|
|
const casePayload = { customerId: tenantId, orderId: actualOrderId, inboundId, containerBookingId,
|
2026-08-08 22:42:49 +08:00
|
|
|
checklistVersionId, remark: context.marker, expectedVersion: 0 }
|
|
|
|
|
const caseResponse = await request(context, 'POST', `${PLATFORM}/cases`, casePayload, context.token,
|
|
|
|
|
{ 'Idempotency-Key': key(context, 'case') })
|
|
|
|
|
const created = context.apiSuccess(MODULE, '创建客户订单柜次关联报关协同单', caseResponse, '', OPTIONS)
|
|
|
|
|
const caseId = id(created?.customsCase?.id)
|
2026-08-17 19:51:54 +08:00
|
|
|
const customsFieldGaps = customsCaseFieldGaps(created)
|
2026-08-11 11:41:41 +08:00
|
|
|
context.assert(MODULE, '报关单绑定本轮实际承载来源分片、入库单、柜次和清单版本', caseResponse,
|
|
|
|
|
Boolean(caseId) && id(created?.customsCase?.tenantId) === tenantId && id(created?.customsCase?.orderId) === actualOrderId
|
2026-08-08 22:42:49 +08:00
|
|
|
&& id(created?.customsCase?.inboundId) === inboundId && id(created?.customsCase?.containerBookingId) === containerBookingId
|
|
|
|
|
&& id(created?.customsCase?.checklistVersionId) === checklistVersionId
|
|
|
|
|
&& created?.customsCase?.status === 'DOCUMENTS_PENDING' && Number(created?.customsCase?.version) === 0,
|
2026-08-11 11:41:41 +08:00
|
|
|
`caseId=${caseId || 'missing'}, parentOrderId=${parentOrderId}, sourceOrderId=${sourceOrderId}, actualOrderId=${actualOrderId}, status=${created?.customsCase?.status || 'missing'}`, OPTIONS)
|
2026-08-17 19:51:54 +08:00
|
|
|
context.assert(MODULE, '报关列表单号、订单入库柜次、清单版本、状态与更新时间字段完整', caseResponse,
|
|
|
|
|
customsFieldGaps.length === 0, `missing=${customsFieldGaps.join(',') || 'none'}`, OPTIONS)
|
2026-08-08 22:42:49 +08:00
|
|
|
if (!caseId) return
|
|
|
|
|
|
|
|
|
|
const caseReplay = await request(context, 'POST', `${PLATFORM}/cases`, casePayload, context.token,
|
|
|
|
|
{ 'Idempotency-Key': key(context, 'case') })
|
|
|
|
|
context.assert(MODULE, '创建报关单幂等重放返回同一聚合', caseReplay,
|
|
|
|
|
codeOf(caseReplay) === '200' && id(caseReplay.body?.data?.customsCase?.id) === caseId,
|
|
|
|
|
`HTTP=${caseReplay.http}, code=${codeOf(caseReplay)}, caseId=${id(caseReplay.body?.data?.customsCase?.id) || 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const forgedCaseResponse = await request(context, 'POST', `${PLATFORM}/cases`, {
|
|
|
|
|
...casePayload, containerBookingId: '999999999999999999', remark: `${context.marker}-forged-container`,
|
|
|
|
|
}, context.token, { 'Idempotency-Key': key(context, 'forged-case') })
|
|
|
|
|
context.addStep(MODULE, '伪造未承载客户订单的柜次被拒绝', forgedCaseResponse, failure(forgedCaseResponse),
|
|
|
|
|
`HTTP=${forgedCaseResponse.http}, code=${codeOf(forgedCaseResponse)}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const firstUploadPath = `${MERCHANT}/cases/${caseId}/documents/upload`
|
|
|
|
|
const firstUpload = await upload(context, firstUploadPath, { itemCode: 'INVOICE', expectedVersion: 0 },
|
|
|
|
|
`${context.marker} invoice version 1`, `${context.marker}-invoice-v1.txt`, merchantToken, key(context, 'invoice-v1'))
|
|
|
|
|
const document1 = context.apiSuccess(MODULE, '客户上传首版报关发票', firstUpload, '', OPTIONS)
|
|
|
|
|
const document1Id = id(document1?.id)
|
|
|
|
|
context.assert(MODULE, '客户文件版本、哈希、上传主体和业务归属已持久化', firstUpload,
|
|
|
|
|
Boolean(document1Id) && document1?.checklistItemCode === 'INVOICE' && Number(document1?.documentVersion) === 1
|
|
|
|
|
&& document1?.status === 'UPLOADED' && document1?.uploadedByType === 'MERCHANT'
|
|
|
|
|
&& /^[0-9a-f]{64}$/.test(String(document1?.fileHash || '')),
|
|
|
|
|
`documentId=${document1Id || 'missing'}, version=${document1?.documentVersion ?? 'missing'}`, OPTIONS)
|
|
|
|
|
if (!document1Id) return
|
|
|
|
|
|
|
|
|
|
const firstUploadReplay = await upload(context, firstUploadPath, { itemCode: 'INVOICE', expectedVersion: 0 },
|
|
|
|
|
`${context.marker} invoice version 1`, `${context.marker}-invoice-v1.txt`, merchantToken, key(context, 'invoice-v1'))
|
|
|
|
|
context.assert(MODULE, '客户文件上传在聚合版本变化后仍可幂等重放', firstUploadReplay,
|
|
|
|
|
codeOf(firstUploadReplay) === '200' && id(firstUploadReplay.body?.data?.id) === document1Id
|
|
|
|
|
&& Number(firstUploadReplay.body?.data?.documentVersion) === 1,
|
|
|
|
|
`HTTP=${firstUploadReplay.http}, code=${codeOf(firstUploadReplay)}, documentId=${id(firstUploadReplay.body?.data?.id) || 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
let current = await detail(context, caseId)
|
|
|
|
|
context.assert(MODULE, '资料齐备后进入 REVIEWING 且版本递增', current.response,
|
|
|
|
|
current.data?.customsCase?.status === 'REVIEWING' && Number(current.data?.customsCase?.version) === 1,
|
|
|
|
|
`status=${current.data?.customsCase?.status || 'missing'}, version=${current.data?.customsCase?.version ?? 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const returnedResponse = await request(context, 'POST', `${PLATFORM}/cases/${caseId}/documents/${document1Id}/review`, {
|
|
|
|
|
decision: 'RETURN', reason: `${context.marker}-发票金额需补正`, supplementDueAt: '2026-09-30T18:00:00', expectedVersion: 1,
|
|
|
|
|
}, context.token, { 'Idempotency-Key': key(context, 'return-v1') })
|
|
|
|
|
const returned = context.apiSuccess(MODULE, '平台退回具体文件并创建补件请求', returnedResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '退回进入 SUPPLEMENT_REQUIRED 并保留原因、时限和历史', returnedResponse,
|
|
|
|
|
returned?.customsCase?.status === 'SUPPLEMENT_REQUIRED' && Number(returned?.customsCase?.version) === 2
|
|
|
|
|
&& returned?.supplements?.some?.((item) => item.status === 'OPEN' && item.checklistItemCode === 'INVOICE'
|
|
|
|
|
&& String(item.reason || '').includes(context.marker) && Boolean(item.dueAt))
|
|
|
|
|
&& returned?.history?.some?.((item) => item.action === 'REVIEW_DOCUMENT_RETURN' && id(item.resourceId) === document1Id),
|
|
|
|
|
`status=${returned?.customsCase?.status || 'missing'}, supplements=${returned?.supplements?.length ?? 'n/a'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const replacement = await upload(context, firstUploadPath, { itemCode: 'INVOICE', expectedVersion: 2 },
|
|
|
|
|
`${context.marker} invoice version 2 corrected`, `${context.marker}-invoice-v2.txt`, merchantToken, key(context, 'invoice-v2'))
|
|
|
|
|
const document2 = context.apiSuccess(MODULE, '客户补件替换发票并生成新版本', replacement, '', OPTIONS)
|
|
|
|
|
const document2Id = id(document2?.id)
|
|
|
|
|
context.assert(MODULE, '替换文件保留上一版本引用且版本递增', replacement,
|
|
|
|
|
Boolean(document2Id) && Number(document2?.documentVersion) === 2 && id(document2?.replacesDocumentId) === document1Id
|
|
|
|
|
&& document2?.status === 'UPLOADED',
|
|
|
|
|
`documentId=${document2Id || 'missing'}, replaces=${id(document2?.replacesDocumentId) || 'missing'}`, OPTIONS)
|
|
|
|
|
if (!document2Id) return
|
|
|
|
|
|
|
|
|
|
current = await detail(context, caseId)
|
|
|
|
|
context.assert(MODULE, '补件任务解决且历史文件版本未覆盖', current.response,
|
|
|
|
|
current.data?.supplements?.some?.((item) => item.status === 'RESOLVED' && id(item.resolvedDocumentId) === document2Id)
|
|
|
|
|
&& current.data?.documents?.some?.((item) => id(item.id) === document1Id && item.status === 'SUPERSEDED')
|
|
|
|
|
&& current.data?.documents?.some?.((item) => id(item.id) === document2Id && Number(item.documentVersion) === 2),
|
|
|
|
|
`documents=${current.data?.documents?.length ?? 'n/a'}, supplements=${current.data?.supplements?.length ?? 'n/a'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const approveResponse = await request(context, 'POST', `${PLATFORM}/cases/${caseId}/documents/${document2Id}/review`, {
|
|
|
|
|
decision: 'APPROVE', reason: '资料完整', supplementDueAt: null, expectedVersion: 3,
|
|
|
|
|
}, context.token, { 'Idempotency-Key': key(context, 'approve-v2') })
|
|
|
|
|
const approved = context.apiSuccess(MODULE, '平台审核通过补件版本', approveResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '全部必填文件通过后进入 READY_TO_SUBMIT', approveResponse,
|
|
|
|
|
approved?.customsCase?.status === 'READY_TO_SUBMIT' && Number(approved?.customsCase?.version) === 4,
|
|
|
|
|
`status=${approved?.customsCase?.status || 'missing'}, version=${approved?.customsCase?.version ?? 'missing'}`, OPTIONS)
|
|
|
|
|
|
2026-08-15 11:05:58 +08:00
|
|
|
const supplierCreateResponse = await context.request('POST', '/api/platform/v1/suppliers?tenantId=0', {
|
2026-08-17 14:46:44 +08:00
|
|
|
name: `报关代理-${context.marker}`.slice(0, 128), category: 'CUSTOMS', region: '深圳',
|
2026-08-15 11:05:58 +08:00
|
|
|
legalRepresentative: '验收法人', contactName: '报关联系人', phone: '13800000000',
|
|
|
|
|
email: 'customs-acceptance@example.com', address: '深圳市验收地址',
|
|
|
|
|
businessLicenseNo: `CUSTOMS${context.marker.replace(/\D/g, '')}`.slice(0, 64), licenseFileId: null,
|
|
|
|
|
status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: context.marker,
|
|
|
|
|
})
|
|
|
|
|
const customsAgentId = id(context.apiSuccess(MODULE, '创建本轮正式 ACTIVE 报关代理供应商',
|
|
|
|
|
supplierCreateResponse, `marker=${context.marker}`, OPTIONS))
|
|
|
|
|
if (!customsAgentId) return
|
|
|
|
|
const supplierDetailResponse = await context.request('GET',
|
|
|
|
|
`/api/platform/v1/suppliers/${encodeURIComponent(customsAgentId)}?tenantId=0`)
|
|
|
|
|
const customsAgent = context.apiSuccess(MODULE, '回查本轮报关代理供应商', supplierDetailResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '代理提交使用本轮正式 CUSTOMS 供应商身份', supplierDetailResponse,
|
|
|
|
|
id(customsAgent?.id) === customsAgentId && customsAgent?.status === 'ACTIVE'
|
|
|
|
|
&& customsAgent?.category === 'CUSTOMS' && customsAgent?.remark === context.marker,
|
|
|
|
|
`customsAgentId=${id(customsAgent?.id) || 'missing'}, customsAgentName=${customsAgent?.name || 'missing'}`, OPTIONS)
|
|
|
|
|
if (id(customsAgent?.id) !== customsAgentId || customsAgent?.status !== 'ACTIVE'
|
|
|
|
|
|| customsAgent?.category !== 'CUSTOMS') return
|
|
|
|
|
context.createdResources.phase3CustomsSupplier = { marker: context.marker, supplierId: customsAgentId,
|
|
|
|
|
supplierName: customsAgent.name, status: customsAgent.status }
|
|
|
|
|
const batchPayload = { customsAgentId, externalReference: `EXT-${suffix}`,
|
2026-08-08 22:42:49 +08:00
|
|
|
note: context.marker, expectedVersion: 4 }
|
|
|
|
|
const batchResponse = await request(context, 'POST', `${PLATFORM}/cases/${caseId}/submission-batches`, batchPayload,
|
|
|
|
|
context.token, { 'Idempotency-Key': key(context, 'batch') })
|
|
|
|
|
const batch = context.apiSuccess(MODULE, '生成代理提交批次并冻结文件版本', batchResponse, '', OPTIONS)
|
|
|
|
|
const batchId = id(batch?.id)
|
|
|
|
|
let snapshot = []
|
|
|
|
|
try { snapshot = JSON.parse(batch?.fileSnapshotJson || '[]') } catch { /* asserted below */ }
|
|
|
|
|
context.assert(MODULE, '代理批次冻结当前文件 ID、版本和哈希', batchResponse,
|
|
|
|
|
Boolean(batchId) && batch?.status === 'SUBMITTED' && Number(batch?.batchVersion) === 1
|
|
|
|
|
&& snapshot.some?.((item) => id(item.documentId) === document2Id && Number(item.documentVersion) === 2
|
|
|
|
|
&& /^[0-9a-f]{64}$/.test(String(item.fileHash || ''))),
|
|
|
|
|
`batchId=${batchId || 'missing'}, frozenFiles=${snapshot.length}`, OPTIONS)
|
|
|
|
|
if (!batchId) return
|
|
|
|
|
|
|
|
|
|
const batchReplay = await request(context, 'POST', `${PLATFORM}/cases/${caseId}/submission-batches`, batchPayload,
|
|
|
|
|
context.token, { 'Idempotency-Key': key(context, 'batch') })
|
|
|
|
|
context.assert(MODULE, '代理提交幂等重放返回同一冻结批次', batchReplay,
|
|
|
|
|
codeOf(batchReplay) === '200' && id(batchReplay.body?.data?.id) === batchId,
|
|
|
|
|
`HTTP=${batchReplay.http}, code=${codeOf(batchReplay)}, batchId=${id(batchReplay.body?.data?.id) || 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const staleSubmit = await request(context, 'POST', `${PLATFORM}/cases/${caseId}/submission-batches`, {
|
2026-08-15 11:05:58 +08:00
|
|
|
...batchPayload, externalReference: `EXT-DIFFERENT-${suffix}`,
|
2026-08-08 22:42:49 +08:00
|
|
|
}, context.token, { 'Idempotency-Key': key(context, 'batch') })
|
|
|
|
|
context.addStep(MODULE, '同一幂等键不同代理提交请求被拒绝', staleSubmit,
|
|
|
|
|
staleSubmit.http === 200 && codeOf(staleSubmit) !== '200',
|
|
|
|
|
`HTTP=${staleSubmit.http}, code=${codeOf(staleSubmit)}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const receivedUpload = await upload(context, `${PLATFORM}/submission-batches/${batchId}/receipts/upload`, {
|
|
|
|
|
receiptType: 'RECEIVED', visibility: 'PLATFORM', note: context.marker, expectedVersion: 5,
|
|
|
|
|
}, `${context.marker} agent received`, `${context.marker}-received.txt`, context.token, key(context, 'receipt-received'))
|
|
|
|
|
const received = context.apiSuccess(MODULE, '上传平台可见代理接收回执', receivedUpload, '', OPTIONS)
|
|
|
|
|
const receivedId = id(received?.id)
|
|
|
|
|
context.assert(MODULE, '接收回执版本和平台可见范围正确', receivedUpload,
|
|
|
|
|
Boolean(receivedId) && received?.receiptType === 'RECEIVED' && received?.visibility === 'PLATFORM'
|
|
|
|
|
&& Number(received?.receiptVersion) === 1,
|
|
|
|
|
`receiptId=${receivedId || 'missing'}, visibility=${received?.visibility || 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const releaseUpload = await upload(context, `${PLATFORM}/submission-batches/${batchId}/receipts/upload`, {
|
|
|
|
|
receiptType: 'RELEASED', visibility: 'CUSTOMER', note: `${context.marker}-放行`, expectedVersion: 6,
|
|
|
|
|
}, `${context.marker} customs released`, `${context.marker}-released.txt`, context.token, key(context, 'receipt-released'))
|
|
|
|
|
const released = context.apiSuccess(MODULE, '上传客户可见海关放行回执', releaseUpload, '', OPTIONS)
|
|
|
|
|
const releasedId = id(released?.id)
|
|
|
|
|
context.assert(MODULE, '放行回执类型、版本和客户可见范围正确', releaseUpload,
|
|
|
|
|
Boolean(releasedId) && released?.receiptType === 'RELEASED' && released?.visibility === 'CUSTOMER'
|
|
|
|
|
&& Number(released?.receiptVersion) === 1,
|
|
|
|
|
`receiptId=${releasedId || 'missing'}, visibility=${released?.visibility || 'missing'}`, OPTIONS)
|
|
|
|
|
if (!releasedId) return
|
|
|
|
|
|
|
|
|
|
current = await detail(context, caseId)
|
|
|
|
|
context.assert(MODULE, '报关放行推进 CLEARANCE_COMPLETED 并保留完整稳定历史', current.response,
|
|
|
|
|
current.data?.customsCase?.status === 'CLEARANCE_COMPLETED' && Number(current.data?.customsCase?.version) === 7
|
|
|
|
|
&& current.data?.documents?.some?.((item) => id(item.id) === document2Id && item.status === 'FROZEN')
|
|
|
|
|
&& current.data?.history?.some?.((item) => item.action === 'SUBMIT_AGENT_BATCH' && id(item.resourceId) === batchId)
|
|
|
|
|
&& current.data?.history?.some?.((item) => item.action === 'UPLOAD_RECEIPT' && id(item.resourceId) === releasedId
|
|
|
|
|
&& item.toStatus === 'CLEARANCE_COMPLETED' && Boolean(item.operatorId) && Boolean(item.createdAt)),
|
|
|
|
|
`status=${current.data?.customsCase?.status || 'missing'}, history=${current.data?.history?.length ?? 'n/a'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const platformDownload = await download(context, `${PLATFORM}/cases/${caseId}/receipts/${releasedId}/download`, context.token)
|
|
|
|
|
context.assert(MODULE, '平台授权下载真实放行回执内容', platformDownload,
|
|
|
|
|
platformDownload.http === 200 && platformDownload.contentType.includes('text/plain') && platformDownload.bytes.length > 0,
|
|
|
|
|
`HTTP=${platformDownload.http}, bytes=${platformDownload.bytes.length}, contentType=${platformDownload.contentType || 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const merchantDetailResponse = await request(context, 'GET', `${MERCHANT}/cases/${caseId}`, undefined, merchantToken)
|
|
|
|
|
const merchantDetail = context.apiSuccess(MODULE, '客户查询本企业报关进度和可见回执', merchantDetailResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '客户详情过滤平台回执并保留客户放行回执', merchantDetailResponse,
|
|
|
|
|
id(merchantDetail?.customsCase?.tenantId) === tenantId
|
|
|
|
|
&& merchantDetail?.receipts?.some?.((item) => id(item.id) === releasedId)
|
|
|
|
|
&& !merchantDetail?.receipts?.some?.((item) => id(item.id) === receivedId),
|
|
|
|
|
`receipts=${merchantDetail?.receipts?.length ?? 'n/a'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const merchantDownload = await download(context, `${MERCHANT}/cases/${caseId}/receipts/${releasedId}/download`, merchantToken)
|
|
|
|
|
context.assert(MODULE, '客户授权下载客户可见放行回执', merchantDownload,
|
|
|
|
|
merchantDownload.http === 200 && merchantDownload.bytes.length > 0,
|
|
|
|
|
`HTTP=${merchantDownload.http}, bytes=${merchantDownload.bytes.length}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const hiddenDownload = await download(context, `${MERCHANT}/cases/${caseId}/receipts/${receivedId}/download`, merchantToken)
|
|
|
|
|
context.addStep(MODULE, '客户下载平台可见回执被拒绝', hiddenDownload,
|
|
|
|
|
hiddenDownload.http !== 200 || String(hiddenDownload.body?.code || '') !== '200',
|
|
|
|
|
`HTTP=${hiddenDownload.http}, code=${hiddenDownload.body?.code || 'binary'}, bytes=${hiddenDownload.bytes.length}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const forgedReference = await request(context, 'POST', `${PLATFORM}/cases/${caseId}/documents`, {
|
|
|
|
|
itemCode: 'INVOICE', fileId: released?.fileId, expectedVersion: 7,
|
|
|
|
|
}, context.token, { 'Idempotency-Key': key(context, 'forged-file-reference') })
|
|
|
|
|
context.addStep(MODULE, '回执 fileId 不能伪造为客户报关资料引用', forgedReference, failure(forgedReference),
|
|
|
|
|
`HTTP=${forgedReference.http}, code=${codeOf(forgedReference)}`, OPTIONS)
|
|
|
|
|
|
2026-09-09 02:54:09 +08:00
|
|
|
if (!(await verifyDomesticCustomsEvidence(context, { containerBookingId, loadingPlanId, warehouseId }))) {
|
|
|
|
|
context.addStep(MODULE, '国内报关资料与PDA证据必须完成后才允许进入出仓确认', null, false,
|
2026-08-26 14:56:33 +08:00
|
|
|
`containerBookingId=${containerBookingId}, loadingPlanId=${loadingPlanId}, warehouseId=${warehouseId}`, OPTIONS)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-08 22:42:49 +08:00
|
|
|
context.createdResources.phase3Customs = {
|
2026-08-11 11:41:41 +08:00
|
|
|
marker: context.marker, tenantId, parentOrderId, orderId: actualOrderId, inboundId, containerBookingId, loadingPlanId,
|
2026-08-08 22:42:49 +08:00
|
|
|
customsCaseId: caseId, submissionBatchId: batchId, releasedReceiptId: releasedId,
|
|
|
|
|
finalStatus: 'CLEARANCE_COMPLETED',
|
|
|
|
|
}
|
|
|
|
|
}
|