396 lines
24 KiB
JavaScript
396 lines
24 KiB
JavaScript
import {
|
|
CONTAINER_CUSTOMS_FIXTURE_KINDS,
|
|
CONTAINER_CUSTOMS_MODULE,
|
|
EXACT_50_MB,
|
|
validateContainerCustomsFixtures,
|
|
validateRouteContract,
|
|
xlsxMagic,
|
|
zeroDrift,
|
|
} from './container-customs-acceptance-matrix.mjs'
|
|
|
|
const MODULE = CONTAINER_CUSTOMS_MODULE
|
|
const OPTIONS = { scenario: MODULE, deploymentBlocker: true }
|
|
const id = (value) => value == null ? '' : String(value)
|
|
const code = (response) => String(response?.body?.code ?? '')
|
|
const success = (response) => response?.http === 200 && code(response) === '200'
|
|
const a0409 = (response) => response?.http === 200 && code(response) === 'A0409'
|
|
const evidence = (response, extra = '') => `${extra}${extra ? '; ' : ''}HTTP=${response?.http ?? 'missing'}, code=${code(response) || 'missing'}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`
|
|
const key = (context, value) => `${context.marker}-container-customs-${value}`.slice(0, 128)
|
|
|
|
function blocker(context, name, reason) {
|
|
context.addCoverageGap(MODULE, MODULE, name, reason, 'contract-or-fixture', { deploymentBlocker: true })
|
|
context.addStep(MODULE, name, null, false, reason, OPTIONS)
|
|
throw new Error(`blocking coverage gap: ${name}: ${reason}`)
|
|
}
|
|
|
|
function resolvePreparation(context) {
|
|
const fixtures = context.createdResources?.phase3ContainerCustomsFixtures
|
|
const routes = context.context?.phase3ContainerCustomsRoutes
|
|
const requestedKinds = fixtures?.requestedKinds || CONTAINER_CUSTOMS_FIXTURE_KINDS
|
|
if (!validateContainerCustomsFixtures(fixtures, context.marker, requestedKinds)) {
|
|
blocker(context, '三类LOADED整柜fixture', '缺少本轮全买单、纯退税、混合三柜正式链路数据')
|
|
}
|
|
if (!validateRouteContract(routes)) {
|
|
blocker(context, '整柜报关最终HTTP契约', '后端Controller/OpenAPI与Admin/Client最终路径尚未对齐,禁止猜测URL发请求')
|
|
}
|
|
return { fixtures, routes, requestedKinds }
|
|
}
|
|
|
|
async function platformDetail(context, routes, fixture) {
|
|
return routes.platformDetail(context, { loadingPlanId: id(fixture.loadingPlanId) })
|
|
}
|
|
|
|
async function assertZeroDrift(context, name, routes, fixture, before, response, acceptedCodes = ['A0409']) {
|
|
const after = await platformDetail(context, routes, fixture)
|
|
context.assert(MODULE, name, response,
|
|
response?.http === 200 && acceptedCodes.includes(code(response))
|
|
&& success(after) && zeroDrift(before.body?.data, after.body?.data),
|
|
evidence(response, `afterRequestId=${after.requestId || 'missing'}, afterTraceId=${after.traceId || 'missing'}`), OPTIONS)
|
|
}
|
|
|
|
async function runAllBuy(context, routes, fixture) {
|
|
const detail = await platformDetail(context, routes, fixture)
|
|
context.assert(MODULE, '全买单柜LOADED详情', detail, success(detail)
|
|
&& detail.body?.data?.inbounds?.every?.((item) => item.taxMode === 'BUY_DECLARATION'), evidence(detail), OPTIONS)
|
|
|
|
const xlsx = await routes.buyDeclarationXlsx(context, { loadingPlanId: id(fixture.loadingPlanId) })
|
|
context.assert(MODULE, '买单XLSX下载窗口返回真实工作簿', xlsx,
|
|
xlsx.http === 200 && xlsxMagic(xlsx.bytes),
|
|
`HTTP=${xlsx.http}, bytes=${xlsx.bytes?.length ?? 0}, contentType=${xlsx.contentType || 'missing'}`, OPTIONS)
|
|
|
|
const startBody = { expectedVersion: Number(detail.body?.data?.version), remark: context.marker }
|
|
const started = await routes.platformStart(context, fixture, startBody, key(context, 'all-buy-start'))
|
|
context.assert(MODULE, '全买单柜开始报关', started,
|
|
success(started) && started.body?.data?.status === 'DECLARING', evidence(started), OPTIONS)
|
|
|
|
const completeBody = { expectedVersion: Number(started.body?.data?.version), remark: context.marker }
|
|
const completed = await routes.platformComplete(context, fixture, completeBody, key(context, 'all-buy-complete'))
|
|
context.assert(MODULE, '全买单柜完成并触发自动离仓意图', completed,
|
|
success(completed) && completed.body?.data?.status === 'COMPLETED', evidence(completed), OPTIONS)
|
|
const replay = await routes.platformComplete(context, fixture, completeBody, key(context, 'all-buy-complete'))
|
|
context.assert(MODULE, '全买单柜完成同键重放', replay,
|
|
success(replay) && id(replay.body?.data?.aggregateId) === id(completed.body?.data?.aggregateId), evidence(replay), OPTIONS)
|
|
|
|
let outbound = await routes.outboundState(context, fixture)
|
|
if (fixture.autoDispatchMode === 'WAIT_CONDITION') {
|
|
context.assert(MODULE, '自动离仓失败不回滚报关完成', outbound,
|
|
success(outbound) && outbound.body?.data?.customsStatus === 'COMPLETED'
|
|
&& outbound.body?.data?.departed !== true && Boolean(outbound.body?.data?.retryable),
|
|
evidence(outbound), OPTIONS)
|
|
await routes.satisfyOutboundPrerequisites(context, fixture)
|
|
for (let attempt = 0; attempt < 12 && outbound.body?.data?.departed !== true; attempt += 1) {
|
|
if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, 1_000))
|
|
outbound = await routes.outboundState(context, fixture)
|
|
}
|
|
}
|
|
context.assert(MODULE, '自动或待条件重试后整柜单次离仓', outbound,
|
|
success(outbound) && outbound.body?.data?.departed === true
|
|
&& Number(outbound.body?.data?.departureHistoryCount) === 1,
|
|
evidence(outbound), OPTIONS)
|
|
}
|
|
|
|
async function uploadBoundary(context, routes, fixture, customer, size, suffix) {
|
|
const content = new Uint8Array(size)
|
|
content[0] = 0x43; content[content.length - 1] = 0x58
|
|
return routes.merchantUploadDocument(context, fixture, customer, {
|
|
fileName: `${context.marker}-${suffix}.customblob`, contentType: 'application/octet-stream', content,
|
|
inboundIds: customer.inboundIds.map(id), expectedVersion: customer.expectedVersion,
|
|
}, key(context, suffix))
|
|
}
|
|
|
|
async function runRefund(context, routes, fixture) {
|
|
const customers = fixture.customers || []
|
|
if (customers.length < 2) blocker(context, '纯退税多客户fixture', '至少需要两个客户和可交叉验证的多对多入仓范围')
|
|
const first = customers[0]
|
|
const exact = await uploadBoundary(context, routes, fixture, first, EXACT_50_MB, 'exact-50m')
|
|
context.assert(MODULE, '任意格式50MiB边界成功', exact, success(exact), evidence(exact), OPTIONS)
|
|
const tooLargeBefore = await platformDetail(context, routes, fixture)
|
|
const tooLarge = await uploadBoundary(context, routes, fixture, first, EXACT_50_MB + 1, 'over-50m')
|
|
await assertZeroDrift(context, '50MiB+1拒绝且零漂移', routes, fixture, tooLargeBefore, tooLarge,
|
|
['A0400', 'A0409', 'A0422'])
|
|
|
|
const crossCustomer = await routes.merchantDetail(context, fixture, customers[1], first.customerToken)
|
|
context.assert(MODULE, '客户隔离阻止查看其他客户资料', crossCustomer,
|
|
!success(crossCustomer), evidence(crossCustomer), OPTIONS)
|
|
const hiddenBuy = await routes.buyDeclarationXlsx(context, { loadingPlanId: id(fixture.loadingPlanId), token: first.customerToken })
|
|
context.assert(MODULE, '客户侧隐藏买单XLSX', hiddenBuy, hiddenBuy.http !== 200 || !xlsxMagic(hiddenBuy.bytes),
|
|
`HTTP=${hiddenBuy.http}, bytes=${hiddenBuy.bytes?.length ?? 0}`, OPTIONS)
|
|
|
|
for (const customer of customers) {
|
|
const document = await routes.merchantUploadDocument(context, fixture, customer, customer.document,
|
|
key(context, `upload-${customer.customerId}`))
|
|
context.assert(MODULE, `客户${customer.customerId}上传多对多资料`, document, success(document), evidence(document), OPTIONS)
|
|
customer.documentId = id(document.body?.data?.documentId)
|
|
customer.documentVersion = Number(document.body?.data?.source?.version)
|
|
}
|
|
|
|
const mutable = customers[0]
|
|
const replaced = await routes.merchantReplaceDocument(context, fixture, mutable, {
|
|
...mutable.replacement, documentId: mutable.documentId, expectedVersion: mutable.documentVersion,
|
|
}, key(context, 'replace'))
|
|
context.assert(MODULE, '客户覆盖资料生成新版本', replaced, success(replaced), evidence(replaced), OPTIONS)
|
|
const scoped = await routes.merchantUpdateScope(context, fixture, mutable, {
|
|
documentId: mutable.documentId, inboundIds: mutable.updatedInboundIds.map(id),
|
|
expectedVersion: Number(replaced.body?.data?.source?.version),
|
|
}, key(context, 'scope'))
|
|
context.assert(MODULE, '客户修改多对多关联', scoped, success(scoped), evidence(scoped), OPTIONS)
|
|
const deleted = await routes.merchantDeleteDocument(context, fixture, mutable, {
|
|
documentId: mutable.documentId, expectedVersion: Number(scoped.body?.data?.source?.version),
|
|
}, key(context, 'delete'))
|
|
context.assert(MODULE, '客户删除未冻结资料', deleted, success(deleted), evidence(deleted), OPTIONS)
|
|
|
|
const beforeMissingStart = await platformDetail(context, routes, fixture)
|
|
const missingStart = await routes.platformStart(context, fixture, {
|
|
expectedVersion: Number(beforeMissingStart.body?.data?.version), remark: 'missing-document-negative',
|
|
}, key(context, 'missing-start'))
|
|
await assertZeroDrift(context, '缺退税资料禁止开始且零漂移', routes, fixture, beforeMissingStart, missingStart)
|
|
|
|
const restored = await routes.merchantUploadDocument(context, fixture, mutable, mutable.document,
|
|
key(context, `restore-${mutable.customerId}`))
|
|
context.assert(MODULE, '删除后重新上传恢复完整退税覆盖', restored, success(restored), evidence(restored), OPTIONS)
|
|
mutable.documentId = id(restored.body?.data?.documentId)
|
|
mutable.documentVersion = Number(restored.body?.data?.source?.version)
|
|
|
|
const ready = await platformDetail(context, routes, fixture)
|
|
const started = await routes.platformStart(context, fixture, {
|
|
expectedVersion: Number(ready.body?.data?.version), remark: context.marker,
|
|
}, key(context, 'refund-start'))
|
|
context.assert(MODULE, '纯退税柜资料齐全后开始并冻结', started,
|
|
success(started) && started.body?.data?.status === 'DECLARING'
|
|
&& started.body?.data?.documents?.every?.((item) => item.frozen === true),
|
|
evidence(started), OPTIONS)
|
|
|
|
const frozenSnapshot = started.body?.data
|
|
const lockedOperations = [
|
|
['上传', () => routes.merchantUploadDocument(context, fixture, mutable, {
|
|
...mutable.document, fileName: `${context.marker}-locked.customblob`, content: Uint8Array.from([1]),
|
|
}, key(context, 'locked-upload'))],
|
|
['覆盖', () => routes.merchantReplaceDocument(context, fixture, mutable, {
|
|
...mutable.replacement, documentId: mutable.documentId, expectedVersion: mutable.documentVersion,
|
|
}, key(context, 'locked-replace'))],
|
|
['改关联', () => routes.merchantUpdateScope(context, fixture, mutable, {
|
|
documentId: mutable.documentId, inboundIds: mutable.inboundIds.map(id), expectedVersion: mutable.documentVersion,
|
|
}, key(context, 'locked-scope'))],
|
|
['删除', () => routes.merchantDeleteDocument(context, fixture, mutable, {
|
|
documentId: mutable.documentId, expectedVersion: mutable.documentVersion,
|
|
}, key(context, 'locked-delete'))],
|
|
]
|
|
for (const [label, request] of lockedOperations) {
|
|
const response = await request()
|
|
await assertZeroDrift(context, `开始后${label}锁定A0409零漂移`, routes, fixture,
|
|
{ body: { data: frozenSnapshot } }, response)
|
|
}
|
|
|
|
const documents = frozenSnapshot?.documents || []
|
|
for (const [index, document] of documents.entries()) {
|
|
const completion = await routes.platformUploadCompletion(context, fixture, {
|
|
documentId: id(document.documentId), expectedVersion: Number(document.source?.version),
|
|
fileName: `${context.marker}-completion-${index + 1}.done`,
|
|
contentType: 'application/x-customs-completion', content: Uint8Array.from([0x43, index + 1]),
|
|
}, key(context, `completion-${index + 1}`))
|
|
context.assert(MODULE, `逐份上传完成文件${index + 1}`, completion,
|
|
success(completion)
|
|
&& zeroDrift(completion.body?.data?.inboundIds, document.inboundIds),
|
|
evidence(completion, `sourceDocumentId=${id(document.documentId)}`), OPTIONS)
|
|
if (index + 1 < documents.length) {
|
|
const beforeEarlyComplete = await platformDetail(context, routes, fixture)
|
|
const early = await routes.platformComplete(context, fixture, {
|
|
expectedVersion: Number(beforeEarlyComplete.body?.data?.version), remark: 'not-last-completion',
|
|
}, key(context, `early-complete-${index + 1}`))
|
|
await assertZeroDrift(context, '最后一份完成文件前禁止整柜完成', routes, fixture, beforeEarlyComplete, early)
|
|
}
|
|
}
|
|
const completedReady = await platformDetail(context, routes, fixture)
|
|
const completed = await routes.platformComplete(context, fixture, {
|
|
expectedVersion: Number(completedReady.body?.data?.version), remark: context.marker,
|
|
}, key(context, 'refund-complete'))
|
|
context.assert(MODULE, '最后一份完成文件后纯退税柜完成', completed,
|
|
success(completed) && completed.body?.data?.status === 'COMPLETED', evidence(completed), OPTIONS)
|
|
|
|
const firstDocument = documents[0]
|
|
if (firstDocument) {
|
|
const platformDownload = await routes.platformDownload(context, fixture, {
|
|
documentId: id(firstDocument.documentId), completion: true,
|
|
})
|
|
context.assert(MODULE, 'Admin下载完成文件窗口', platformDownload,
|
|
platformDownload.http === 200 && platformDownload.bytes?.length > 0,
|
|
`HTTP=${platformDownload.http}, bytes=${platformDownload.bytes?.length ?? 0}`, OPTIONS)
|
|
const merchantDownload = await routes.merchantDownload(context, fixture, mutable, {
|
|
documentId: id(firstDocument.documentId), completion: true,
|
|
})
|
|
context.assert(MODULE, 'Client仅下载本客户完成文件窗口', merchantDownload,
|
|
merchantDownload.http === 200 && merchantDownload.bytes?.length > 0,
|
|
`HTTP=${merchantDownload.http}, bytes=${merchantDownload.bytes?.length ?? 0}`, OPTIONS)
|
|
}
|
|
}
|
|
|
|
async function runMixedConcurrency(context, routes, fixture) {
|
|
const initial = await platformDetail(context, routes, fixture)
|
|
const initialData = initial.body?.data
|
|
const buy = (initialData?.inbounds || []).filter((item) => item.taxMode === 'BUY_DECLARATION')
|
|
const refund = (initialData?.inbounds || []).filter((item) => item.taxMode === 'TAX_REFUND')
|
|
context.assert(MODULE, '混合柜平台详情精确包含BUY与TAX长ID票', initial,
|
|
success(initial) && buy.length > 0 && refund.length > 0
|
|
&& initialData.inbounds.every((item) => /^\d{16,}$/.test(id(item.inboundId))),
|
|
evidence(initial, `buy=${buy.length}, tax=${refund.length}`), OPTIONS)
|
|
context.assert(MODULE, '退税资料未覆盖前状态精确阻断开始', initial,
|
|
initialData?.status === 'WAITING_CUSTOMER_DOCUMENTS'
|
|
&& refund.every((item) => item.covered === false),
|
|
evidence(initial, `status=${initialData?.status || 'missing'}`), OPTIONS)
|
|
|
|
const customer = fixture.customers?.[0]
|
|
if (!customer) blocker(context, '混合柜客户fixture', '缺少本轮退税客户与正式登录凭据')
|
|
const merchant = await routes.merchantDetail(context, fixture, customer, customer.customerToken)
|
|
context.assert(MODULE, 'Client只见本客户TAX票且完全隐藏BUY票', merchant,
|
|
success(merchant) && merchant.body?.data?.inbounds?.length === refund.length
|
|
&& merchant.body.data.inbounds.every((item) => item.taxMode === 'TAX_REFUND')
|
|
&& !merchant.body.data.inbounds.some((item) => fixture.buyInboundIds.includes(id(item.inboundId))),
|
|
evidence(merchant), OPTIONS)
|
|
const merchantPlatform = await routes.merchantPlatformDetail(context, fixture)
|
|
context.assert(MODULE, 'Client身份禁止读取平台整柜报关入口', merchantPlatform,
|
|
merchantPlatform?.http === 403 || (merchantPlatform?.http === 200
|
|
&& ['A0401', 'A0403', 'S0503'].includes(code(merchantPlatform))),
|
|
evidence(merchantPlatform), OPTIONS)
|
|
|
|
const exportResult = await routes.buyDeclarationXlsx(context, { loadingPlanId: id(fixture.loadingPlanId) })
|
|
context.assert(MODULE, '买单导出只处理实际装柜BUY行', exportResult,
|
|
exportResult.http === 200 && xlsxMagic(exportResult.bytes)
|
|
&& Number(exportResult.task?.processedRows) === Number(fixture.expectedBuyRows)
|
|
&& Number(fixture.expectedBuyRows) > 0,
|
|
`processedRows=${exportResult.task?.processedRows ?? 'missing'}, expectedBuyRows=${fixture.expectedBuyRows}, bytes=${exportResult.bytes?.length ?? 0}`, OPTIONS)
|
|
|
|
const premature = await routes.platformStart(context, fixture, {
|
|
expectedVersion: Number(initialData?.version), confirm: true, remark: context.marker,
|
|
}, key(context, 'mixed-premature-start'))
|
|
await assertZeroDrift(context, '退税资料缺失时开始精确A0409且零漂移', routes, fixture, initial, premature)
|
|
|
|
const uploadKey = key(context, 'mixed-tax-upload')
|
|
const uploaded = await routes.merchantUploadDocument(context, fixture, customer, customer.document, uploadKey)
|
|
context.assert(MODULE, 'Client上传资料只覆盖本柜TAX票', uploaded,
|
|
success(uploaded) && uploaded.body?.data?.inboundIds?.length === refund.length
|
|
&& uploaded.body.data.inboundIds.every((value) => customer.inboundIds.includes(id(value)))
|
|
&& !uploaded.body.data.inboundIds.some((value) => fixture.buyInboundIds.includes(id(value))),
|
|
evidence(uploaded), OPTIONS)
|
|
const uploadReplay = await routes.merchantUploadDocument(context, fixture, customer, customer.document, uploadKey)
|
|
context.assert(MODULE, '退税资料上传同键重放返回同一长ID文件', uploadReplay,
|
|
success(uploadReplay) && id(uploadReplay.body?.data?.documentId) === id(uploaded.body?.data?.documentId),
|
|
evidence(uploadReplay), OPTIONS)
|
|
customer.documentId = id(uploaded.body?.data?.documentId)
|
|
customer.documentVersion = Number(uploaded.body?.data?.source?.version)
|
|
|
|
const ready = await platformDetail(context, routes, fixture)
|
|
context.assert(MODULE, 'TAX票全部覆盖后平台准确进入READY_TO_START', ready,
|
|
success(ready) && ready.body?.data?.status === 'READY_TO_START'
|
|
&& ready.body.data.taxRefundCount === ready.body.data.coveredTaxRefundCount,
|
|
evidence(ready), OPTIONS)
|
|
const body = { expectedVersion: Number(ready.body?.data?.version), confirm: true, remark: context.marker }
|
|
const concurrentKey = key(context, 'mixed-start-concurrent')
|
|
const responses = await Promise.all([
|
|
routes.platformStart(context, fixture, body, concurrentKey),
|
|
routes.platformStart(context, fixture, body, concurrentKey),
|
|
])
|
|
const startsConverged = responses.every(success)
|
|
&& zeroDrift(responses[0].body?.data, responses[1].body?.data)
|
|
context.assert(MODULE, '开始并发双击收敛同一结果', responses[1],
|
|
startsConverged,
|
|
responses.map((response) => evidence(response)).join('; '), OPTIONS)
|
|
if (!startsConverged) {
|
|
context.addCoverageGap(MODULE, OPTIONS.scenario, '同键并发start幂等收敛',
|
|
'同一Idempotency-Key与相同载荷未收敛,已停止后续完成写入并保留现场',
|
|
'backend-idempotency', { deploymentBlocker: true })
|
|
return
|
|
}
|
|
|
|
const different = await routes.platformStart(context, fixture, { ...body, remark: 'different' }, concurrentKey)
|
|
context.assert(MODULE, '同幂等键异载荷A0409', different, a0409(different), evidence(different), OPTIONS)
|
|
const frozen = responses.find(success)?.body?.data
|
|
const locked = await routes.merchantUpdateScope(context, fixture, customer, {
|
|
documentId: customer.documentId, inboundIds: customer.inboundIds.map(id), expectedVersion: customer.documentVersion,
|
|
}, key(context, 'locked-scope'))
|
|
await assertZeroDrift(context, '开始后资料范围锁定A0409零漂移', routes, fixture, { body: { data: frozen } }, locked)
|
|
|
|
const completionRequest = {
|
|
documentId: customer.documentId, expectedVersion: 0,
|
|
finalConfirmation: true, fileName: `${context.marker}-mixed-result.done`,
|
|
contentType: 'application/x-customs-completion', content: Uint8Array.from([0x52, 0x45, 0x53]),
|
|
}
|
|
const completionKey = key(context, 'mixed-completion')
|
|
const completion = await routes.platformUploadCompletion(context, fixture, completionRequest, completionKey)
|
|
const completionReplay = success(completion)
|
|
? await routes.platformUploadCompletion(context, fixture, completionRequest, completionKey) : null
|
|
const completionConverged = success(completion) && success(completionReplay)
|
|
&& id(completion.body?.data?.documentId) === id(completionReplay.body?.data?.documentId)
|
|
&& id(completion.body?.data?.completion?.fileId) === id(completionReplay.body?.data?.completion?.fileId)
|
|
context.assert(MODULE, '最后一份TAX完成文件二次确认并同键重放', completionReplay || completion,
|
|
completionConverged,
|
|
[completion, completionReplay].filter(Boolean).map((response) => evidence(response)).join('; '), OPTIONS)
|
|
if (!completionConverged) {
|
|
context.addCoverageGap(MODULE, OPTIONS.scenario, '最后一份完成文件二次确认',
|
|
'完成文件未成功或同键重放未收敛,已停止后续自动离仓写入',
|
|
'completion-idempotency', { deploymentBlocker: true })
|
|
return
|
|
}
|
|
const completionDifferent = await routes.platformUploadCompletion(context, fixture,
|
|
{ ...completionRequest, content: Uint8Array.from([0x44, 0x49, 0x46]) }, completionKey)
|
|
context.assert(MODULE, '完成文件同键异载荷A0409', completionDifferent,
|
|
a0409(completionDifferent), evidence(completionDifferent), OPTIONS)
|
|
|
|
let completed = await platformDetail(context, routes, fixture)
|
|
context.assert(MODULE, '混合柜最后一份完成文件后整柜COMPLETED', completed,
|
|
success(completed) && completed.body?.data?.status === 'COMPLETED', evidence(completed), OPTIONS)
|
|
fixture.customsStatus = 'COMPLETED'
|
|
let outbound = await routes.outboundState(context, fixture)
|
|
for (let attempt = 0; attempt < 12 && outbound.body?.data?.departed !== true; attempt += 1) {
|
|
await new Promise((resolve) => setTimeout(resolve, 5_000))
|
|
outbound = await routes.outboundState(context, fixture)
|
|
}
|
|
context.assert(MODULE, '整柜完成后自动离仓只写一次SYSTEM历史', outbound,
|
|
success(outbound) && outbound.body?.data?.departed === true
|
|
&& outbound.body?.data?.departureHistoryCount === 1
|
|
&& outbound.body?.data?.timeline?.filter((item) => item.module === 'OUTBOUND_DISPATCH'
|
|
&& item.action === 'CONFIRM_DISPATCH' && id(item.operatorId) === '0'
|
|
&& item.operatorName === 'SYSTEM').length === 1,
|
|
evidence(outbound, `history=${outbound.body?.data?.departureHistoryCount ?? 'missing'}`), OPTIONS)
|
|
|
|
const queryPage = await context.request('GET', `/api/platform/v1/carrier-schedule-queries?${new URLSearchParams({
|
|
containerBookingId: id(fixture.containerBookingId), page: '1', size: '20',
|
|
})}`)
|
|
const queries = queryPage.body?.data?.records || []
|
|
const matching = queries.filter((item) => item?.query?.triggerSource === 'OUTBOUND_CONFIRM'
|
|
&& id(item?.query?.containerBookingId) === id(fixture.containerBookingId))
|
|
context.assert(MODULE, '自动离仓只创建一个OUTBOUND_CONFIRM长ID任务', queryPage,
|
|
success(queryPage) && matching.length === 1 && /^\d{16,}$/.test(id(matching[0]?.query?.id)),
|
|
evidence(queryPage, `matches=${matching.length}`), OPTIONS)
|
|
if (matching.length === 1) {
|
|
const queryDetail = await context.request('GET', `/api/platform/v1/carrier-schedule-queries/${encodeURIComponent(id(matching[0].query.id))}`)
|
|
context.assert(MODULE, '船节点事实与SYSTEM自动创建历史均唯一', queryDetail,
|
|
success(queryDetail)
|
|
&& queryDetail.body.data.nodes.filter((item) => item.standardNode === 'DEPARTED_WAREHOUSE'
|
|
&& item.providerCode === 'FIRSTLEG' && item.providerEventId === 'OUTBOUND_CONFIRM').length === 1
|
|
&& queryDetail.body.data.histories.filter((item) => item.action === 'AUTO_CREATE'
|
|
&& id(item.operatorId) === '0' && item.operatorName === 'SYSTEM').length === 1,
|
|
evidence(queryDetail), OPTIONS)
|
|
}
|
|
|
|
const completionDownload = await routes.merchantDownload(context, fixture, customer, {
|
|
documentId: customer.documentId, completion: true,
|
|
})
|
|
context.assert(MODULE, '整柜完成后Client可下载自身TAX完成文件', completionDownload,
|
|
completionDownload.http === 200 && completionDownload.bytes?.length > 0,
|
|
`HTTP=${completionDownload.http}, bytes=${completionDownload.bytes?.length ?? 0}`, OPTIONS)
|
|
}
|
|
|
|
export async function run(context) {
|
|
const { fixtures, routes, requestedKinds } = resolvePreparation(context)
|
|
const byKind = new Map(fixtures.containers.map((item) => [item.kind, item]))
|
|
if (requestedKinds.includes(CONTAINER_CUSTOMS_FIXTURE_KINDS[0])) {
|
|
await runAllBuy(context, routes, byKind.get(CONTAINER_CUSTOMS_FIXTURE_KINDS[0]))
|
|
}
|
|
if (requestedKinds.includes(CONTAINER_CUSTOMS_FIXTURE_KINDS[1])) {
|
|
await runRefund(context, routes, byKind.get(CONTAINER_CUSTOMS_FIXTURE_KINDS[1]))
|
|
}
|
|
if (requestedKinds.includes(CONTAINER_CUSTOMS_FIXTURE_KINDS[2])) {
|
|
await runMixedConcurrency(context, routes, byKind.get(CONTAINER_CUSTOMS_FIXTURE_KINDS[2]))
|
|
}
|
|
}
|