test(firstleg): add mixed container customs acceptance
This commit is contained in:
@@ -26,13 +26,14 @@ function blocker(context, name, reason) {
|
||||
function resolvePreparation(context) {
|
||||
const fixtures = context.createdResources?.phase3ContainerCustomsFixtures
|
||||
const routes = context.context?.phase3ContainerCustomsRoutes
|
||||
if (!validateContainerCustomsFixtures(fixtures, context.marker)) {
|
||||
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 }
|
||||
return { fixtures, routes, requestedKinds }
|
||||
}
|
||||
|
||||
async function platformDetail(context, routes, fixture) {
|
||||
@@ -224,32 +225,167 @@ async function runRefund(context, routes, fixture) {
|
||||
}
|
||||
|
||||
async function runMixedConcurrency(context, routes, fixture) {
|
||||
const before = await platformDetail(context, routes, fixture)
|
||||
const body = { expectedVersion: Number(before.body?.data?.version), remark: context.marker }
|
||||
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 context.request('GET',
|
||||
`/api/platform/v1/customs/loading-plans/${encodeURIComponent(id(fixture.loadingPlanId))}`,
|
||||
undefined, customer.customerToken)
|
||||
context.assert(MODULE, 'Client身份禁止读取平台整柜报关入口', merchantPlatform,
|
||||
merchantPlatform?.http === 403 || (merchantPlatform?.http === 200
|
||||
&& ['A0401', 'A0403', 'S0503'].includes(codeOf(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],
|
||||
responses.every(success) && zeroDrift(responses[0].body?.data, responses[1].body?.data),
|
||||
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[0].body?.data
|
||||
const customer = fixture.customers?.[0]
|
||||
if (!customer?.documentId) blocker(context, '混合柜冻结文件fixture', '混合柜缺少已上传退税客户资料')
|
||||
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: customer.documentVersion,
|
||||
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 completions = await Promise.all([
|
||||
routes.platformUploadCompletion(context, fixture, completionRequest, completionKey),
|
||||
routes.platformUploadCompletion(context, fixture, completionRequest, completionKey),
|
||||
])
|
||||
context.assert(MODULE, '最后一份TAX完成文件并发同键收敛同一结果', completions[1],
|
||||
completions.every(success)
|
||||
&& id(completions[0].body?.data?.documentId) === id(completions[1].body?.data?.documentId)
|
||||
&& id(completions[0].body?.data?.completion?.fileId) === id(completions[1].body?.data?.completion?.fileId),
|
||||
completions.map((response) => evidence(response)).join('; '), OPTIONS)
|
||||
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 } = resolvePreparation(context)
|
||||
const { fixtures, routes, requestedKinds } = resolvePreparation(context)
|
||||
const byKind = new Map(fixtures.containers.map((item) => [item.kind, item]))
|
||||
await runAllBuy(context, routes, byKind.get(CONTAINER_CUSTOMS_FIXTURE_KINDS[0]))
|
||||
await runRefund(context, routes, byKind.get(CONTAINER_CUSTOMS_FIXTURE_KINDS[1]))
|
||||
await runMixedConcurrency(context, routes, byKind.get(CONTAINER_CUSTOMS_FIXTURE_KINDS[2]))
|
||||
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]))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user