Files
baiyue-firstleg/scripts/admin-test-env-modules/container-customs-acceptance-contract.test.mjs
T
2026-10-21 21:30:24 +08:00

227 lines
13 KiB
JavaScript

import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { prepare } from './phase3-container-customs-fixture.mjs'
test('fixed single customer blocks mixed customs before creating orders or loading fixtures', async () => {
await assert.rejects(prepare({ options: { fixedCustomerId: '123' } }), /停止订单及装柜造数/)
})
import { verifyFormalDeclarationAndExceptions, verifyMixedReceiptIsolation } from './phase3-container-customs.mjs'
import {
CONTAINER_CUSTOMS_FIXTURE_KINDS,
CONTAINER_CUSTOMS_MATRIX,
EXACT_50_MB,
READ_ONLY_DB_EVIDENCE,
REQUIRED_ROUTE_OPERATIONS,
validateContainerCustomsFixtures,
validateRouteContract,
xlsxMagic,
zeroDrift,
} from './container-customs-acceptance-matrix.mjs'
const source = readFileSync(new URL('./phase3-container-customs.mjs', import.meta.url), 'utf8')
const fixtureSource = readFileSync(new URL('./phase3-container-customs-fixture.mjs', import.meta.url), 'utf8')
const entrySource = readFileSync(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8')
test('matrix covers three isolated LOADED container kinds', () => {
assert.deepEqual(CONTAINER_CUSTOMS_FIXTURE_KINDS,
['ALL_BUY_DECLARATION', 'ALL_TAX_REFUND', 'MIXED'])
const fixtures = { marker: 'M', containers: CONTAINER_CUSTOMS_FIXTURE_KINDS.map((kind, index) => ({
marker: 'M', kind, status: 'LOADED', loadingPlanId: String(100 + index),
containerBookingId: String(200 + index),
inbounds: [{ inboundId: String(300 + index), customerId: String(400 + index),
taxMode: kind === 'ALL_BUY_DECLARATION' ? 'BUY_DECLARATION' : 'TAX_REFUND' }],
})) }
assert.equal(validateContainerCustomsFixtures(fixtures, 'M'), true)
assert.equal(validateContainerCustomsFixtures({ marker: 'M', containers: [fixtures.containers[2]] },
'M', ['MIXED']), true)
assert.equal(validateContainerCustomsFixtures({ ...fixtures, marker: 'OLD' }, 'M'), false)
})
test('mixed fixture producer is registered and uses only public HTTP composition', () => {
assert.match(entrySource, /CONFIGURED_MODULES[^\n]*'phase3-container-customs'/)
assert.match(entrySource, /\['phase3-container-customs', \['merchant-qualification-fixture'\]\]/)
assert.match(entrySource, /phase3-container-customs-fixture\.mjs/)
assert.match(fixtureSource, /primary: 'BUY_DECLARATION', incremental: 'TAX_REFUND'/)
assert.match(fixtureSource, /phase3-loading\.mjs/)
assert.match(fixtureSource, /phase3-loading-finalize\.mjs/)
assert.match(fixtureSource, /LOADING_LIST_BUY_DECLARATION_ITEMS/)
assert.match(fixtureSource, /merchantQualificationFixture/)
assert.match(fixtureSource, /merchantPlatformDetail/)
assert.doesNotMatch(fixtureSource, /customerToken:/)
assert.doesNotMatch(fixtureSource, /\b(?:INSERT\s+INTO|DELETE\s+FROM|UPDATE\s+(?:fl_|ll_|wh_|ord_))/i)
})
test('coverage matrix freezes every requested product boundary', () => {
const text = CONTAINER_CUSTOMS_MATRIX.map((item) => `${item.scenario}:${item.expected}`).join('\n')
for (const token of ['买单XLSX', '多对多', '覆盖', '改关联', '删除', '冻结', '回执确认', '报关行执行', '完税文件',
'最后上传', '混合', '缺资料', '开始后锁定', '客户隔离', '买单隐藏', '50MiB', '任意格式',
'幂等', '并发', 'A0409', '零漂移', '自动离仓', 'Outbox', '历史', 'Admin与Client权限']) {
assert.match(text, new RegExp(token))
}
})
test('route adapter fails closed until final backend and frontend contracts align', () => {
assert.equal(validateRouteContract({}), false)
const routes = { version: 'final', ...Object.fromEntries(REQUIRED_ROUTE_OPERATIONS.map((name) => [name, () => null])) }
assert.equal(validateRouteContract(routes), true)
assert.match(source, /后端Controller\/OpenAPI与Admin\/Client最终路径尚未对齐,禁止猜测URL发请求/)
assert.doesNotMatch(source, /https?:\/\//)
})
test('HTTP scenario contains file, state, idempotency, concurrency and zero-drift probes', () => {
assert.doesNotMatch(source, /\bcodeOf\(/)
for (const token of ['EXACT_50_MB + 1', '.customblob', 'application/octet-stream',
'缺退税资料禁止开始且零漂移', '开始后资料范围锁定A0409零漂移',
"['上传'", "['覆盖'", "['改关联'", "['删除'", '开始后${label}锁定A0409零漂移',
'首次发送报关行并生成独立批次', '混合柜冻结资料首次发送报关行并生成独立批次',
'回执未确认且未发送执行时禁止上传完税文件', '客户确认整份回执后仍处于报关中', '真实报关完税文件齐全后纯退税柜完成', '客户确认回执后仍未完成整柜',
'混合柜平台详情精确包含BUY与TAX长ID票', 'Client只见本客户TAX票且完全隐藏BUY票',
'买单导出只处理实际装柜BUY行', '混合柜最后一份完成文件后整柜COMPLETED',
'自动离仓只写一次SYSTEM历史', '自动离仓只创建一个OUTBOUND_CONFIRM长ID任务',
'documentId: customer.documentId, expectedVersion: 0',
'完成文件未成功或同键重放未收敛,已停止后续自动离仓写入',
'已停止后续完成写入并保留现场',
'自动离仓失败不回滚报关完成', '自动或待条件重试后整柜单次离仓',
'同幂等键异载荷A0409', 'Promise.all', 'requestId=', 'traceId=', 'xlsxMagic']) {
assert.match(source, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
}
assert.equal(EXACT_50_MB, 52_428_800)
assert.equal(zeroDrift({ a: 1 }, { a: 1 }), true)
assert.equal(xlsxMagic(Uint8Array.from([0x50, 0x4b, 0x03, 0x04])), true)
})
test('database evidence list covers atomicity, history, dispatch and outbox without embedding SQL writes', () => {
for (const table of ['fl_customs_container', 'fl_customs_customer_document_inbound',
'fl_customs_completion_document', 'fl_customs_final_document', 'fl_customs_command', 'fl_customs_container_history',
'fl_outbound_dispatch', 'fl_event_outbox']) assert.ok(READ_ONLY_DB_EVIDENCE.includes(table), table)
assert.doesNotMatch(source, /\b(?:INSERT|UPDATE|DELETE)\s+(?:INTO|FROM|[a-z_])/i)
})
function runtimeContext() {
return { marker: 'OFFLINE-CUSTOMS', asserted: [],
assert(_module, name, _response, passed) { assert.ok(passed, name); this.asserted.push(name) },
addCoverageGap() {}, addStep() {},
}
}
const response = (data, code = '200') => ({ http: 200, body: { code, data: structuredClone(data) } })
function formalAdapter({ prematurelyClose = false, invalidOtherWrites = false } = {}) {
const container = { loadingPlanId: '1', containerBookingId: '2', containerNo: 'TESTU1234567',
status: 'DECLARING', version: 8, brokerSubmittedAt: '2026-09-30T10:00:00',
documents: [{ documentId: '10', source: { fileId: '11' }, receiptStatus: 'CONFIRMED' }], inbounds: [] }
let batch = null
return {
platformDetail: async () => response(container),
platformCreateDeclaration: async () => { batch = { id: '100', status: 'READY', version: 0, results: [] }; return response(batch) },
platformDeclarationBatches: async () => response([batch]),
platformSubmitDeclaration: async (_ctx, _fixture, _id, request) => {
assert.equal(request.expectedVersion, batch.version)
batch.status = 'SUBMITTED'; batch.version += 1; batch.customsEntryNo = request.customsEntryNo
return response(batch)
},
platformRecordClearance: async (_ctx, _fixture, _id, request) => {
assert.equal(request.expectedVersion, batch.version)
if (request.resultType === 'OTHER' && !request.resolution) {
if (invalidOtherWrites) batch.results.push({ id: 'invalid', resultStatus: 'OPEN' })
return response(null, 'A0400')
}
const result = { id: String(200 + batch.results.length), resultType: request.resultType,
resultStatus: request.resolved ? 'RESOLVED' : 'OPEN' }
batch.results.push(result)
batch.status = prematurelyClose && request.resolved ? 'CLOSED'
: batch.results.some((item) => item.resultStatus === 'OPEN') ? 'RESULT_PENDING' : 'CLOSED'
batch.version += 1
return response(result)
},
platformResolveClearance: async (_ctx, _fixture, resultId, request) => {
assert.equal(request.expectedVersion, batch.version)
const result = batch.results.find((item) => item.id === resultId)
result.resultStatus = 'RESOLVED'; batch.version += 1
batch.status = batch.results.some((item) => item.resultStatus === 'OPEN') ? 'RESULT_PENDING' : 'CLOSED'
return response(result)
},
platformClearanceResults: async () => response(batch.results),
}
}
test('formal declaration runs every manual exception, retains OPEN gates and keeps customs document facts', async () => {
const ctx = runtimeContext(); const fixture = {}
await verifyFormalDeclarationAndExceptions(ctx, formalAdapter(), fixture)
assert.equal(fixture.declarationBatchId, '100')
assert.equal(fixture.clearanceResultIds.length, 5)
for (const type of ['INSPECTION', 'SUPPLEMENT_REQUIRED', 'DETAINED', 'OTHER']) {
assert.ok(ctx.asserted.includes(`平台人工标记海关异常${type}`))
}
assert.ok(ctx.asserted.includes('海关人工处理保留客户资料回执确认原柜与报关四态'))
})
test('formal probe fails when a resolved result closes a batch with unresolved exceptions', async () => {
await assert.rejects(verifyFormalDeclarationAndExceptions(runtimeContext(),
formalAdapter({ prematurelyClose: true }), {}), /不能提前关闭申报批次/)
})
test('OTHER missing remark probe rejects hidden writes even when API returns expected error', async () => {
await assert.rejects(verifyFormalDeclarationAndExceptions(runtimeContext(),
formalAdapter({ invalidOtherWrites: true }), {}), /必须填写备注且批次结果零漂移/)
})
function mixedAdapter({ contaminateForeignConfirmation = false } = {}) {
const docs = [{ documentId: 'A', receipt: { version: 1, fileId: 'RA' }, receiptStatus: 'PENDING' },
{ documentId: 'B', receipt: null, receiptStatus: null }]
const container = { version: 2, status: 'DECLARING', documents: docs, brokerSubmittedAt: null }
const confirmations = new Set()
return {
platformDetail: async () => response(container),
platformUploadReceipt: async (_ctx, _fixture, request) => {
const doc = docs.find((item) => item.documentId === request.documentId)
assert.equal(request.expectedVersion, doc.receipt?.version || 0)
doc.receipt = { version: (doc.receipt?.version || 0) + 1, fileId: `R${doc.documentId}` }
doc.receiptStatus = 'PENDING'
return response(doc)
},
merchantConfirmReceipt: async (_ctx, _fixture, customer, request, commandKey) => {
if (customer.documentId !== request.documentId) {
if (contaminateForeignConfirmation) docs[0].receiptStatus = 'PENDING'
return response(null, 'A0404')
}
const doc = docs.find((item) => item.documentId === request.documentId)
assert.equal(request.expectedVersion, doc.receipt.version)
if (!confirmations.has(commandKey)) {
doc.receiptStatus = request.confirm ? 'CONFIRMED' : 'REJECTED'
confirmations.add(commandKey)
}
return response(container)
},
platformSubmitToBroker: async () => response(null, 'A0409'),
platformCreateDeclaration: async () => response(null, 'A0409'),
}
}
const mixedFixture = () => ({ customers: [{ documentId: 'A', customerId: '1' },
{ documentId: 'B', customerId: '2' }] })
test('multi-customer receipt flow rejects, replaces and confirms only the affected customer', async () => {
const ctx = runtimeContext()
const result = await verifyMixedReceiptIsolation(ctx, mixedAdapter(), mixedFixture(),
response({ receipt: { version: 1 } }))
assert.ok(result.body.data.documents.every((item) => item.receiptStatus === 'CONFIRMED'))
assert.ok(ctx.asserted.includes('未全部客户确认禁止发送报关行执行'))
assert.ok(ctx.asserted.includes('未全部确认禁止创建正式申报批次'))
})
test('cross-customer receipt denial also requires prior confirmation remain unchanged', async () => {
await assert.rejects(verifyMixedReceiptIsolation(runtimeContext(),
mixedAdapter({ contaminateForeignConfirmation: true }), mixedFixture(),
response({ receipt: { version: 1 } })), /第二客户不能确认其他客户回执且原确认保持/)
})
test('second customer credentials stay in adapter memory and are not exported with report resources', () => {
assert.match(fixtureSource, /publishEnvironment: false/)
assert.match(fixtureSource, /customerTokens\.get/)
assert.match(fixtureSource, /customers\.length < 2/)
const customerData = fixtureSource.slice(fixtureSource.indexOf('const customers ='), fixtureSource.indexOf('const buyInboundIds ='))
assert.doesNotMatch(customerData, /(?:sessionToken|accessToken|customerToken|password)/)
assert.doesNotMatch(fixtureSource, /\b(?:INSERT\s+INTO|DELETE\s+FROM|UPDATE\s+(?:fl_|ll_|wh_|ord_))/i)
})