test: strengthen customs and fulfillment acceptance boundaries
This commit is contained in:
@@ -33,6 +33,11 @@ function resolvePreparation(context) {
|
||||
if (!validateRouteContract(routes)) {
|
||||
blocker(context, '整柜报关最终HTTP契约', '后端Controller/OpenAPI与Admin/Client最终路径尚未对齐,禁止猜测URL发请求')
|
||||
}
|
||||
if (requestedKinds.includes('MIXED') && ['platformDeclarationBatches', 'platformCreateDeclaration',
|
||||
'platformSubmitDeclaration', 'platformClearanceResults', 'platformRecordClearance',
|
||||
'platformResolveClearance'].some((name) => typeof routes[name] !== 'function')) {
|
||||
blocker(context, '正式申报与人工海关结果HTTP契约', '缺少已核对Controller的正式申报或海关结果适配器')
|
||||
}
|
||||
return { fixtures, routes, requestedKinds }
|
||||
}
|
||||
|
||||
@@ -260,6 +265,168 @@ async function runRefund(context, routes, fixture) {
|
||||
|
||||
}
|
||||
|
||||
export function customsDocumentFacts(container) {
|
||||
return { loadingPlanId: id(container?.loadingPlanId), containerBookingId: id(container?.containerBookingId),
|
||||
containerNo: container?.containerNo, status: container?.status,
|
||||
documents: container?.documents || [], inbounds: container?.inbounds || [],
|
||||
brokerSubmittedAt: container?.brokerSubmittedAt, brokerSubmission: container?.brokerSubmission }
|
||||
}
|
||||
|
||||
export async function verifyMixedReceiptIsolation(context, routes, fixture, firstReceipt) {
|
||||
const [first, other] = fixture.customers
|
||||
const receiptVersion = Number(firstReceipt.body?.data?.receipt?.version)
|
||||
const firstConfirmed = await routes.merchantConfirmReceipt(context, fixture, first, {
|
||||
documentId: first.documentId, expectedVersion: receiptVersion, confirm: true, remark: context.marker,
|
||||
}, key(context, 'mixed-receipt-confirm'))
|
||||
const firstReplay = await routes.merchantConfirmReceipt(context, fixture, first, {
|
||||
documentId: first.documentId, expectedVersion: receiptVersion, confirm: true, remark: context.marker,
|
||||
}, key(context, 'mixed-receipt-confirm'))
|
||||
context.assert(MODULE, '客户确认回执后仍未完成整柜', firstReplay,
|
||||
success(firstConfirmed) && success(firstReplay) && firstReplay.body?.data?.status === 'DECLARING',
|
||||
evidence(firstReplay), OPTIONS)
|
||||
const beforeOther = await platformDetail(context, routes, fixture)
|
||||
const confirmedFirst = beforeOther.body?.data?.documents?.find((item) => id(item.documentId) === first.documentId)
|
||||
for (const customer of fixture.customers.slice(1)) {
|
||||
const uploaded = await routes.platformUploadReceipt(context, fixture, {
|
||||
documentId: customer.documentId, expectedVersion: 0,
|
||||
fileName: `${context.marker}-${customer.customerId}-receipt.customblob`,
|
||||
contentType: 'application/octet-stream', content: Uint8Array.from([0x52, 0x32]),
|
||||
}, key(context, `receipt-${customer.customerId}`))
|
||||
context.assert(MODULE, '第二客户报关回执独立上传', uploaded, success(uploaded), evidence(uploaded), OPTIONS)
|
||||
}
|
||||
const foreignConfirm = await routes.merchantConfirmReceipt(context, fixture, other, {
|
||||
documentId: first.documentId, expectedVersion: receiptVersion, confirm: true, remark: context.marker,
|
||||
}, key(context, 'foreign-receipt-confirm'))
|
||||
const foreignBefore = await platformDetail(context, routes, fixture)
|
||||
context.assert(MODULE, '第二客户不能确认其他客户回执且原确认保持', foreignConfirm,
|
||||
foreignConfirm.http === 200 && ['A0403', 'A0404'].includes(code(foreignConfirm))
|
||||
&& zeroDrift(confirmedFirst, foreignBefore.body?.data?.documents?.find((item) => id(item.documentId) === first.documentId)),
|
||||
evidence(foreignConfirm), OPTIONS)
|
||||
const otherDocument = foreignBefore.body?.data?.documents?.find((item) => id(item.documentId) === other.documentId)
|
||||
const rejected = await routes.merchantConfirmReceipt(context, fixture, other, {
|
||||
documentId: other.documentId, expectedVersion: Number(otherDocument?.receipt?.version),
|
||||
confirm: false, remark: `${context.marker}-回执资料需修改`,
|
||||
}, key(context, 'mixed-other-reject'))
|
||||
const afterReject = await platformDetail(context, routes, fixture)
|
||||
const rejectedDocument = afterReject.body?.data?.documents?.find((item) => id(item.documentId) === other.documentId)
|
||||
context.assert(MODULE, '第二客户打回不撤销首个客户整份确认', rejected,
|
||||
success(rejected) && rejectedDocument?.receiptStatus === 'REJECTED'
|
||||
&& zeroDrift(confirmedFirst, afterReject.body?.data?.documents?.find((item) => id(item.documentId) === first.documentId)),
|
||||
evidence(rejected), OPTIONS)
|
||||
const earlyExecution = await routes.platformSubmitToBroker(context, fixture, {
|
||||
expectedVersion: Number(afterReject.body?.data?.version), confirm: true, remark: context.marker,
|
||||
}, key(context, 'mixed-unconfirmed-execution'))
|
||||
await assertZeroDrift(context, '未全部客户确认禁止发送报关行执行', routes, fixture, afterReject, earlyExecution)
|
||||
const earlyDeclaration = await routes.platformCreateDeclaration(context, fixture, {
|
||||
expectedVersion: Number(afterReject.body?.data?.version), remark: context.marker,
|
||||
}, key(context, 'mixed-unconfirmed-declaration'))
|
||||
await assertZeroDrift(context, '未全部确认禁止创建正式申报批次', routes, fixture, afterReject, earlyDeclaration)
|
||||
const replacement = await routes.platformUploadReceipt(context, fixture, {
|
||||
documentId: other.documentId, expectedVersion: Number(rejectedDocument?.receipt?.version),
|
||||
fileName: `${context.marker}-${other.customerId}-receipt-v2.customblob`,
|
||||
contentType: 'application/octet-stream', content: Uint8Array.from([0x52, 0x33]),
|
||||
}, key(context, 'mixed-other-receipt-replacement'))
|
||||
const afterReplacement = await platformDetail(context, routes, fixture)
|
||||
context.assert(MODULE, '替换打回客户回执只重置该客户待确认且保留他人确认', replacement,
|
||||
success(replacement) && replacement.body?.data?.receiptStatus === 'PENDING'
|
||||
&& zeroDrift(confirmedFirst, afterReplacement.body?.data?.documents?.find((item) => id(item.documentId) === first.documentId)),
|
||||
evidence(replacement), OPTIONS)
|
||||
for (const customer of fixture.customers.slice(1)) {
|
||||
const current = await platformDetail(context, routes, fixture)
|
||||
const document = current.body?.data?.documents?.find((item) => id(item.documentId) === customer.documentId)
|
||||
const confirmation = await routes.merchantConfirmReceipt(context, fixture, customer, {
|
||||
documentId: customer.documentId, expectedVersion: Number(document?.receipt?.version),
|
||||
confirm: true, remark: context.marker,
|
||||
}, key(context, `mixed-other-confirm-${customer.customerId}`))
|
||||
context.assert(MODULE, '剩余客户确认自己当前整份回执', confirmation, success(confirmation), evidence(confirmation), OPTIONS)
|
||||
}
|
||||
const confirmed = await platformDetail(context, routes, fixture)
|
||||
context.assert(MODULE, '同柜全部客户确认齐全仍等待平台发送执行', confirmed,
|
||||
success(confirmed) && confirmed.body?.data?.documents?.every((item) => item.receiptStatus === 'CONFIRMED')
|
||||
&& confirmed.body?.data?.brokerSubmittedAt == null && confirmed.body?.data?.status === 'DECLARING',
|
||||
evidence(confirmed), OPTIONS)
|
||||
return confirmed
|
||||
}
|
||||
|
||||
export async function verifyFormalDeclarationAndExceptions(context, routes, fixture) {
|
||||
const before = await platformDetail(context, routes, fixture)
|
||||
const baseline = customsDocumentFacts(before.body?.data)
|
||||
const created = await routes.platformCreateDeclaration(context, fixture, {
|
||||
expectedVersion: Number(before.body?.data?.version), batchNo: `${context.marker}-DECL`.slice(-64), remark: context.marker,
|
||||
}, key(context, 'formal-create'))
|
||||
context.assert(MODULE, '平台人工创建正式申报批次', created,
|
||||
success(created) && created.body?.data?.status === 'READY', evidence(created), OPTIONS)
|
||||
if (!success(created)) blocker(context, '正式申报批次', '创建正式申报批次失败,保留资料并停止结果写入')
|
||||
const batchId = id(created.body.data.id)
|
||||
const batches = () => routes.platformDeclarationBatches(context, fixture)
|
||||
const batch = async () => {
|
||||
const response = await batches()
|
||||
if (!success(response)) blocker(context, '正式申报批次回查', evidence(response))
|
||||
const value = response.body.data.find((item) => id(item.id) === batchId)
|
||||
if (!value) blocker(context, '正式申报批次回查', '本轮批次不在当前柜列表')
|
||||
return value
|
||||
}
|
||||
const submitted = await routes.platformSubmitDeclaration(context, fixture, batchId, {
|
||||
expectedVersion: Number(created.body.data.version), customsEntryNo: `${context.marker}-ENTRY`.slice(-64), remark: context.marker,
|
||||
}, key(context, 'formal-submit'))
|
||||
context.assert(MODULE, '人工回填报关单号提交正式申报', submitted,
|
||||
success(submitted) && submitted.body?.data?.status === 'SUBMITTED'
|
||||
&& Boolean(submitted.body?.data?.customsEntryNo), evidence(submitted), OPTIONS)
|
||||
if (!success(submitted)) blocker(context, '正式申报提交', '正式提交失败,禁止继续人工录入海关结果')
|
||||
const local = new Date()
|
||||
const occurredAt = `${local.getFullYear()}-${String(local.getMonth() + 1).padStart(2, '0')}-${String(local.getDate()).padStart(2, '0')}T${String(local.getHours()).padStart(2, '0')}:${String(local.getMinutes()).padStart(2, '0')}:${String(local.getSeconds()).padStart(2, '0')}`
|
||||
const missingRemarkBatch = await batch()
|
||||
const invalidOther = await routes.platformRecordClearance(context, fixture, batchId, {
|
||||
expectedVersion: Number(missingRemarkBatch.version), resultType: 'OTHER', occurredAt, resolution: '', resolved: false,
|
||||
}, key(context, 'other-missing-remark'))
|
||||
const afterInvalid = await batch()
|
||||
context.assert(MODULE, '海关其他异常必须填写备注且批次结果零漂移', invalidOther,
|
||||
invalidOther.http === 200 && code(invalidOther) === 'A0400' && zeroDrift(missingRemarkBatch, afterInvalid),
|
||||
evidence(invalidOther), OPTIONS)
|
||||
const opened = []
|
||||
for (const resultType of ['INSPECTION', 'SUPPLEMENT_REQUIRED', 'DETAINED', 'OTHER']) {
|
||||
const current = await batch()
|
||||
const recorded = await routes.platformRecordClearance(context, fixture, batchId, {
|
||||
expectedVersion: Number(current.version), resultType, occurredAt,
|
||||
resolution: `${context.marker}-${resultType}-人工记录`, resolved: false,
|
||||
}, key(context, `clearance-${resultType}`))
|
||||
context.assert(MODULE, `平台人工标记海关异常${resultType}`, recorded,
|
||||
success(recorded) && recorded.body?.data?.resultType === resultType && recorded.body?.data?.resultStatus === 'OPEN',
|
||||
evidence(recorded), OPTIONS)
|
||||
if (!success(recorded)) blocker(context, '人工海关异常', `${resultType}保存失败`)
|
||||
opened.push(id(recorded.body.data.id))
|
||||
}
|
||||
const pending = await batch()
|
||||
const released = await routes.platformRecordClearance(context, fixture, batchId, {
|
||||
expectedVersion: Number(pending.version), resultType: 'RELEASED', occurredAt,
|
||||
resolution: `${context.marker}-人工录入放行短信`, resolved: true,
|
||||
}, key(context, 'clearance-released-with-open'))
|
||||
const stillPending = await batch()
|
||||
context.assert(MODULE, '已有未解除异常时新增已处理结果不能提前关闭申报批次', released,
|
||||
success(released) && stillPending.status === 'RESULT_PENDING'
|
||||
&& stillPending.results.filter((item) => item.resultStatus === 'OPEN').length === opened.length,
|
||||
evidence(released), OPTIONS)
|
||||
for (const [index, resultId] of opened.entries()) {
|
||||
const current = await batch()
|
||||
const resolved = await routes.platformResolveClearance(context, fixture, resultId, {
|
||||
expectedVersion: Number(current.version), resolution: `${context.marker}-平台人工确认解除`,
|
||||
}, key(context, `clearance-resolve-${resultId}`))
|
||||
const after = await batch()
|
||||
context.assert(MODULE, `人工解除海关异常并保留剩余门禁${index + 1}`, resolved,
|
||||
success(resolved) && resolved.body?.data?.resultStatus === 'RESOLVED'
|
||||
&& after.status === (index === opened.length - 1 ? 'CLOSED' : 'RESULT_PENDING')
|
||||
&& after.results.filter((item) => item.resultStatus === 'OPEN').length === opened.length - index - 1,
|
||||
evidence(resolved), OPTIONS)
|
||||
}
|
||||
const results = await routes.platformClearanceResults(context, fixture, batchId)
|
||||
const after = await platformDetail(context, routes, fixture)
|
||||
context.assert(MODULE, '海关人工处理保留客户资料回执确认原柜与报关四态', after,
|
||||
success(results) && results.body?.data?.length === 5 && results.body.data.every((item) => item.resultStatus === 'RESOLVED')
|
||||
&& success(after) && zeroDrift(baseline, customsDocumentFacts(after.body?.data)), evidence(after), OPTIONS)
|
||||
fixture.declarationBatchId = batchId
|
||||
fixture.clearanceResultIds = [...opened, id(released.body?.data?.id)]
|
||||
}
|
||||
|
||||
async function runMixedConcurrency(context, routes, fixture) {
|
||||
const initial = await platformDetail(context, routes, fixture)
|
||||
const initialData = initial.body?.data
|
||||
@@ -278,7 +445,7 @@ async function runMixedConcurrency(context, routes, fixture) {
|
||||
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
|
||||
success(merchant) && merchant.body?.data?.inbounds?.length === customer.inboundIds.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)
|
||||
@@ -303,7 +470,7 @@ async function runMixedConcurrency(context, routes, fixture) {
|
||||
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
|
||||
success(uploaded) && uploaded.body?.data?.inboundIds?.length === customer.inboundIds.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)
|
||||
@@ -314,6 +481,21 @@ async function runMixedConcurrency(context, routes, fixture) {
|
||||
customer.documentId = id(uploaded.body?.data?.documentId)
|
||||
customer.documentVersion = Number(uploaded.body?.data?.source?.version)
|
||||
|
||||
if (fixture.customers.length < 2) blocker(context, '同柜多退税客户', '必须由本轮正式装柜形成两个独立退税客户')
|
||||
for (const other of fixture.customers.slice(1)) {
|
||||
const isolated = await routes.merchantDetail(context, fixture, other)
|
||||
context.assert(MODULE, '第二客户只见自身退税入仓票', isolated,
|
||||
success(isolated) && isolated.body?.data?.inbounds?.length === other.inboundIds.length
|
||||
&& isolated.body.data.inbounds.every((item) => other.inboundIds.includes(id(item.inboundId))),
|
||||
evidence(isolated), OPTIONS)
|
||||
const otherUpload = await routes.merchantUploadDocument(context, fixture, other, other.document,
|
||||
key(context, `mixed-tax-upload-${other.customerId}`))
|
||||
context.assert(MODULE, '第二退税客户独立上传资料', otherUpload,
|
||||
success(otherUpload) && zeroDrift(otherUpload.body?.data?.inboundIds, other.inboundIds), evidence(otherUpload), OPTIONS)
|
||||
if (!success(otherUpload)) blocker(context, '第二客户退税资料', '本轮第二客户资料上传失败,禁止继续开始')
|
||||
other.documentId = id(otherUpload.body.data.documentId)
|
||||
}
|
||||
|
||||
const ready = await platformDetail(context, routes, fixture)
|
||||
context.assert(MODULE, 'TAX票全部覆盖后平台准确进入READY_TO_START', ready,
|
||||
success(ready) && ready.body?.data?.status === 'READY_TO_START'
|
||||
@@ -372,22 +554,12 @@ async function runMixedConcurrency(context, routes, fixture) {
|
||||
const receiptDifferent = await routes.platformUploadReceipt(context, fixture,
|
||||
{ ...receiptRequest, content: Uint8Array.from([0x44, 0x49, 0x46]) }, receiptKey)
|
||||
context.assert(MODULE, '回执同键异载荷A0409', receiptDifferent, a0409(receiptDifferent), evidence(receiptDifferent), OPTIONS)
|
||||
const receiptVersion = Number(receipt.body?.data?.receipt?.version || 0)
|
||||
const receiptConfirm = await routes.merchantConfirmReceipt(context, fixture, customer, {
|
||||
documentId: customer.documentId, expectedVersion: receiptVersion, confirm: true, remark: context.marker,
|
||||
}, key(context, 'mixed-receipt-confirm'))
|
||||
const receiptConfirmReplay = await routes.merchantConfirmReceipt(context, fixture, customer, {
|
||||
documentId: customer.documentId, expectedVersion: receiptVersion, confirm: true, remark: context.marker,
|
||||
}, key(context, 'mixed-receipt-confirm'))
|
||||
context.assert(MODULE, '客户确认回执后仍未完成整柜', receiptConfirmReplay || receiptConfirm,
|
||||
success(receiptConfirm) && success(receiptConfirmReplay)
|
||||
&& receiptConfirm.body?.data?.status === 'DECLARING'
|
||||
&& receiptConfirmReplay.body?.data?.status === 'DECLARING',
|
||||
[receiptConfirm, receiptConfirmReplay].map((response) => evidence(response)).join('; '), OPTIONS)
|
||||
const receiptConfirm = await verifyMixedReceiptIsolation(context, routes, fixture, receipt)
|
||||
const submitted = await routes.platformSubmitToBroker(context, fixture,
|
||||
{ expectedVersion: Number(receiptConfirm.body?.data?.version), confirm: true, remark: context.marker }, key(context, 'mixed-submit-to-broker'))
|
||||
context.assert(MODULE, '混合柜发送确认回执给报关行执行', submitted,
|
||||
success(submitted) && submitted.body?.data?.status === 'DECLARING', evidence(submitted), OPTIONS)
|
||||
await verifyFormalDeclarationAndExceptions(context, routes, fixture)
|
||||
const finalRequest = {
|
||||
documentId: customer.documentId, expectedVersion: 0,
|
||||
fileName: `${context.marker}-mixed-customs-tax-final.pdf`,
|
||||
@@ -408,6 +580,19 @@ async function runMixedConcurrency(context, routes, fixture) {
|
||||
{ ...finalRequest, content: Uint8Array.from([0x44, 0x49, 0x46]) }, finalKey)
|
||||
context.assert(MODULE, '完税文件同键异载荷A0409', finalDifferent, a0409(finalDifferent), evidence(finalDifferent), OPTIONS)
|
||||
|
||||
const partiallyCompleted = await platformDetail(context, routes, fixture)
|
||||
context.assert(MODULE, '首个客户完成文件不能提前完成多客户整柜', partiallyCompleted,
|
||||
success(partiallyCompleted) && partiallyCompleted.body?.data?.status === 'DECLARING', evidence(partiallyCompleted), OPTIONS)
|
||||
for (const other of fixture.customers.slice(1)) {
|
||||
const otherFinal = await routes.platformUploadFinal(context, fixture, {
|
||||
documentId: other.documentId, expectedVersion: 0,
|
||||
fileName: `${context.marker}-${other.customerId}-final.customblob`,
|
||||
contentType: 'application/octet-stream', content: Uint8Array.from([0x46, 0x32]),
|
||||
}, key(context, `mixed-final-${other.customerId}`))
|
||||
context.assert(MODULE, '其他客户真实完成文件独立回传', otherFinal,
|
||||
success(otherFinal) && zeroDrift(otherFinal.body?.data?.inboundIds, other.inboundIds), evidence(otherFinal), OPTIONS)
|
||||
}
|
||||
|
||||
let completed = await platformDetail(context, routes, fixture)
|
||||
context.assert(MODULE, '混合柜最后一份完成文件后整柜COMPLETED', completed,
|
||||
success(completed) && completed.body?.data?.status === 'COMPLETED', evidence(completed), OPTIONS)
|
||||
@@ -451,6 +636,19 @@ async function runMixedConcurrency(context, routes, fixture) {
|
||||
context.assert(MODULE, '整柜完成后Client可下载自身真实报关完税文件', completionDownload,
|
||||
completionDownload.http === 200 && completionDownload.bytes?.length > 0,
|
||||
`HTTP=${completionDownload.http}, bytes=${completionDownload.bytes?.length ?? 0}`, OPTIONS)
|
||||
for (const other of fixture.customers.slice(1)) {
|
||||
const ownDetail = await routes.merchantDetail(context, fixture, other)
|
||||
context.assert(MODULE, '整柜完成后第二客户详情只包含自身资料与完成文件', ownDetail,
|
||||
success(ownDetail) && ownDetail.body?.data?.documents?.length === 1
|
||||
&& ownDetail.body.data.documents.every((item) => id(item.tenantId) === other.customerId
|
||||
&& id(item.documentId) === other.documentId && item.finalDocument?.fileId),
|
||||
evidence(ownDetail), OPTIONS)
|
||||
const foreignDownload = await routes.merchantDownloadFinal(context, fixture, other, {
|
||||
documentId: customer.documentId,
|
||||
})
|
||||
context.assert(MODULE, '第二客户不能下载其他客户完成文件', foreignDownload,
|
||||
foreignDownload.http === 200 && code(foreignDownload) === 'A0404', evidence(foreignDownload), OPTIONS)
|
||||
}
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
|
||||
Reference in New Issue
Block a user