const MODULE = 'phase3-customs' const OPTIONS = { scenario: 'phase3-customs-collaboration', deploymentBlocker: true } const PLATFORM = '/api/platform/v1/customs' const MERCHANT = '/api/merchant/v1/customs' const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin' const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!' 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 } } export async function run(context) { const fixture = context.createdResources?.orchestrationFixtures?.inboundMeasurement || context.createdResources?.orchestrationFixtures?.phase3Container const container = context.createdResources?.phase3Loading || context.createdResources?.phase3ContainerLoading const tenantId = id(fixture?.tenantId) const parentOrderId = id(fixture?.orderId) const inboundId = id(fixture?.inboundId) const containerBookingId = id(container?.containerBookingId) const loadingPlanId = id(container?.loadingPlanId) if (!tenantId || !parentOrderId || !inboundId || !containerBookingId || !['COMPLETED', 'LOADED'].includes(container?.finalStatus)) { context.addStep(MODULE, '取得本轮客户/订单/入库单/柜次隔离 fixture', null, false, `tenantId=${tenantId || 'missing'}, parentOrderId=${parentOrderId || 'missing'}, inboundId=${inboundId || 'missing'}, containerBookingId=${containerBookingId || 'missing'}, loadingPlanId=${loadingPlanId || 'missing'}, containerStatus=${container?.finalStatus || 'missing'}`, OPTIONS) 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, `containerBookingId=${containerBookingId}, tenantId=${tenantId}`, OPTIONS) const carryingSlices = Array.isArray(slices) ? slices.filter((item) => id(item?.tenantId) === tenantId && id(item?.containerBookingId) === containerBookingId && id(item?.parentOrderId) === parentOrderId && item?.splitRole === 'LOADED' && (!loadingPlanId || id(item?.loadingPlanId) === loadingPlanId)) : [] const carryingSlice = carryingSlices.length === 1 ? carryingSlices[0] : null const actualOrderId = id(carryingSlice?.childOrderId) const sourceOrderId = id(carryingSlice?.sourceOrderId) const sourceCarriesLoaded = Boolean(actualOrderId) && actualOrderId === sourceOrderId const legacyLoadedChild = Boolean(actualOrderId) && Boolean(sourceOrderId) && actualOrderId !== sourceOrderId && actualOrderId !== parentOrderId context.assert(MODULE, '报关绑定柜次实际承载的唯一 LOADED 来源分片', slicesResponse, carryingSlices.length === 1 && (sourceCarriesLoaded || legacyLoadedChild) && 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), `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) || !Number.isInteger(Number(carryingSlice?.packageCount)) || Number(carryingSlice.packageCount) <= 0) return const loginResponse = await request(context, 'POST', '/api/auth/login', { username: MERCHANT_USERNAME, password: MERCHANT_PASSWORD, }, '') 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 const casePayload = { customerId: tenantId, orderId: actualOrderId, inboundId, containerBookingId, 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) context.assert(MODULE, '报关单绑定本轮实际承载来源分片、入库单、柜次和清单版本', caseResponse, Boolean(caseId) && id(created?.customsCase?.tenantId) === tenantId && id(created?.customsCase?.orderId) === actualOrderId && 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, `caseId=${caseId || 'missing'}, parentOrderId=${parentOrderId}, sourceOrderId=${sourceOrderId}, actualOrderId=${actualOrderId}, status=${created?.customsCase?.status || 'missing'}`, OPTIONS) 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) const batchPayload = { customsAgentName: `代理-${suffix}`, externalReference: `EXT-${suffix}`, 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`, { ...batchPayload, customsAgentName: '不同请求', }, 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) context.createdResources.phase3Customs = { marker: context.marker, tenantId, parentOrderId, orderId: actualOrderId, inboundId, containerBookingId, loadingPlanId, customsCaseId: caseId, submissionBatchId: batchId, releasedReceiptId: releasedId, finalStatus: 'CLEARANCE_COMPLETED', } }