优化
This commit is contained in:
@@ -11,7 +11,7 @@ import process from 'node:process'
|
||||
const DEFAULT_GATEWAY = 'http://8.163.30.128:9000'
|
||||
const DEFAULT_USERNAME = 'platform_admin'
|
||||
const DEFAULT_PASSWORD = 'ChangeMe123!'
|
||||
const DEFAULT_MODULES = ['customers', 'system', 'master-data', 'orders', 'pricing', 'downloads', 'inbound-weight-ocr', 'inbound-measurement', 'supplemental']
|
||||
const DEFAULT_MODULES = ['customers', 'overseas-storage-fees', 'system', 'master-data', 'orders', 'pricing', 'downloads', 'inbound-weight-ocr', 'inbound-measurement', 'supplemental']
|
||||
const WEIGHT_IMAGE_PATH = 'C:/Users/BY-OY/Desktop/上传图片包/测量图片/测量图片/orig_1cf27695-b2c3-44b3-98e3-c0be8b80f79a.JPG'
|
||||
const CUSTOMER_FIELDS = [
|
||||
'id', 'tenantCode', 'tenantName', 'status', 'legalName', 'legalRepresentative',
|
||||
@@ -391,6 +391,45 @@ class AcceptanceRun {
|
||||
}
|
||||
}
|
||||
|
||||
async prepareInboundMeasurementFixture() {
|
||||
const originalMarker = this.marker
|
||||
const fixtureMarker = `${originalMarker}-MOJIA`
|
||||
const createdKeys = ['orders', 'ocrInbound', 'testInbound']
|
||||
const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound']
|
||||
const createdSnapshot = new Map(createdKeys.map((key) => [key, {
|
||||
present: Object.hasOwn(this.createdResources, key), value: this.createdResources[key],
|
||||
}]))
|
||||
const contextSnapshot = new Map(contextKeys.map((key) => [key, {
|
||||
present: Object.hasOwn(this.context, key), value: this.context[key],
|
||||
}]))
|
||||
|
||||
this.marker = fixtureMarker
|
||||
const extension = await import(new URL('./admin-test-env-modules/orders.mjs', import.meta.url))
|
||||
await this.executeModule('orders-inbound-measurement-fixture', () => extension.run(this))
|
||||
const fixture = this.createdResources.orders
|
||||
this.createdResources.orchestrationFixtures = {
|
||||
...(this.createdResources.orchestrationFixtures || {}),
|
||||
inboundMeasurement: fixture ? {
|
||||
marker: fixtureMarker,
|
||||
tenantId: fixture.tenantId,
|
||||
orderId: fixture.orderId,
|
||||
inboundId: fixture.inboundId,
|
||||
} : { marker: fixtureMarker, failed: true },
|
||||
}
|
||||
|
||||
return () => {
|
||||
this.marker = originalMarker
|
||||
for (const [key, snapshot] of createdSnapshot) {
|
||||
if (snapshot.present) this.createdResources[key] = snapshot.value
|
||||
else delete this.createdResources[key]
|
||||
}
|
||||
for (const [key, snapshot] of contextSnapshot) {
|
||||
if (snapshot.present) this.context[key] = snapshot.value
|
||||
else delete this.context[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async run() {
|
||||
const loginResponse = await this.request('POST', '/api/auth/login', { username: this.options.username, password: this.options.password }, '')
|
||||
const login = this.apiSuccess('authentication', '平台管理员登录', loginResponse)
|
||||
@@ -398,7 +437,19 @@ class AcceptanceRun {
|
||||
else {
|
||||
this.token = login.accessToken
|
||||
const handlers = { customers: () => this.runCustomers() }
|
||||
for (const module of this.options.modules) {
|
||||
for (let moduleIndex = 0; moduleIndex < this.options.modules.length; moduleIndex += 1) {
|
||||
const module = this.options.modules[moduleIndex]
|
||||
let restoreFixture = null
|
||||
const ocrIndex = this.options.modules.indexOf('inbound-weight-ocr')
|
||||
if (module === 'inbound-measurement' && ocrIndex >= 0 && ocrIndex < moduleIndex) {
|
||||
try {
|
||||
restoreFixture = await this.prepareInboundMeasurementFixture()
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : String(error)
|
||||
this.addStep('orders-inbound-measurement-fixture', '创建独立测量入库单', null, false, reason,
|
||||
{ scenario: 'inbound-resource-isolation' })
|
||||
}
|
||||
}
|
||||
let handler = handlers[module]
|
||||
if (!handler) {
|
||||
try {
|
||||
@@ -409,10 +460,15 @@ class AcceptanceRun {
|
||||
const reason = error instanceof Error ? error.message : String(error)
|
||||
this.addStep(module, '模块注册检查', null, false, `无法加载模块: ${reason}`)
|
||||
this.modules.push({ name: module, success: false, passed: 0, failed: 1, fatal: reason })
|
||||
if (restoreFixture) restoreFixture()
|
||||
continue
|
||||
}
|
||||
}
|
||||
await this.executeModule(module, handler)
|
||||
try {
|
||||
await this.executeModule(module, handler)
|
||||
} finally {
|
||||
if (restoreFixture) restoreFixture()
|
||||
}
|
||||
}
|
||||
}
|
||||
const failed = this.steps.filter((step) => !step.passed).length
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
const MODULE = 'overseas-storage-fees'
|
||||
const SCENARIO = 'customer-overseas-storage-fee-lifecycle'
|
||||
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
|
||||
|
||||
const DEFAULT_TIERS = [
|
||||
{ feeType: 'STORAGE', billingUnit: 'CBM', startDay: 1, endDay: 7, unitPrice: 0 },
|
||||
{ feeType: 'STORAGE', billingUnit: 'CBM', startDay: 8, endDay: 30, unitPrice: 0.8 },
|
||||
{ feeType: 'STORAGE', billingUnit: 'CBM', startDay: 31, endDay: 60, unitPrice: 1 },
|
||||
{ feeType: 'STORAGE', billingUnit: 'CBM', startDay: 61, endDay: 90, unitPrice: 1.5 },
|
||||
{ feeType: 'STORAGE', billingUnit: 'CBM', startDay: 91, endDay: null, unitPrice: 2 },
|
||||
{ feeType: 'RETURN_STORAGE', billingUnit: 'PALLET', startDay: 1, endDay: 30, unitPrice: 1.5 },
|
||||
{ feeType: 'RETURN_STORAGE', billingUnit: 'PALLET', startDay: 31, endDay: 60, unitPrice: 2 },
|
||||
{ feeType: 'RETURN_STORAGE', billingUnit: 'PALLET', startDay: 61, endDay: 90, unitPrice: 3 },
|
||||
{ feeType: 'RETURN_STORAGE', billingUnit: 'PALLET', startDay: 91, endDay: null, unitPrice: 4 },
|
||||
]
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
|
||||
function businessFailure(response) {
|
||||
return [400, 401, 403, 404, 409].includes(response?.http)
|
||||
|| (response?.http === 200 && codeOf(response) !== '200')
|
||||
}
|
||||
function missingRoute(response) {
|
||||
return response?.http === 404 || response?.http === 405
|
||||
|| /Not Found|No handler|No static resource/i.test(String(response?.raw || response?.body?.message || ''))
|
||||
}
|
||||
function close(a, b) { return Number(a) === Number(b) }
|
||||
function tiersOf(view) { return Array.isArray(view?.tiers) ? view.tiers : [] }
|
||||
function requestTiers(view) {
|
||||
return tiersOf(view).map((tier) => ({
|
||||
feeType: tier.feeType, billingUnit: tier.billingUnit,
|
||||
startDay: Number(tier.startDay), endDay: tier.endDay == null ? null : Number(tier.endDay),
|
||||
unitPrice: Number(tier.unitPrice),
|
||||
}))
|
||||
}
|
||||
function tierSnapshot(view) {
|
||||
return tiersOf(view).map((tier) => [tier.feeType, tier.billingUnit, tier.startDay, tier.endDay, String(tier.unitPrice)])
|
||||
}
|
||||
function defaultShape(view) {
|
||||
const tiers = tiersOf(view)
|
||||
const storage = tiers.filter((tier) => tier.feeType === 'STORAGE' && tier.billingUnit === 'CBM')
|
||||
const returns = tiers.filter((tier) => tier.feeType === 'RETURN_STORAGE' && tier.billingUnit === 'PALLET')
|
||||
const matches = (actual, expected) => actual.length === expected.length && expected.every((item) => actual.some((tier) => (
|
||||
Number(tier.startDay) === item.startDay && (tier.endDay == null ? null : Number(tier.endDay)) === item.endDay
|
||||
&& close(tier.unitPrice, item.unitPrice)
|
||||
)))
|
||||
return tiers.length === 9 && storage.length === 5 && returns.length === 4
|
||||
&& matches(storage, DEFAULT_TIERS.filter((tier) => tier.feeType === 'STORAGE'))
|
||||
&& matches(returns, DEFAULT_TIERS.filter((tier) => tier.feeType === 'RETURN_STORAGE'))
|
||||
&& storage.some((tier) => close(tier.unitPrice, 0))
|
||||
&& storage.some((tier) => tier.endDay == null && Number(tier.startDay) === 91)
|
||||
&& returns.some((tier) => tier.endDay == null && Number(tier.startDay) === 91)
|
||||
}
|
||||
|
||||
async function createFixtureCustomer(context) {
|
||||
const suffix = context.marker.replace(/\D/g, '').slice(-12)
|
||||
const phone = await context.findAvailablePhone(MODULE)
|
||||
const payload = {
|
||||
tenantName: `海外仓费用验收-${suffix}`, legalName: `海外仓费用验收有限公司-${suffix}`,
|
||||
legalRepresentative: '费用验收法人', contactName: '费用验收联系人', contactPhone: phone,
|
||||
contactEmail: `storage-fee-${suffix}@example.com`, servicePackage: '海外仓费用验收套餐',
|
||||
contactWechat: `fee_${suffix}`, salesOwnerName: '费用验收销售', paymentTermDays: 30,
|
||||
enterpriseType: 'LIMITED_COMPANY', establishedDate: '2020-01-02', registeredCapital: 1000000,
|
||||
registeredCapitalCurrency: 'CNY', registeredAddress: '深圳市南山区海外仓费用验收地址', internalRemark: context.marker,
|
||||
}
|
||||
const response = await context.request('POST', '/api/platform/v1/iam/tenants', payload)
|
||||
const data = context.apiSuccess(MODULE, '创建海外仓费用隔离客户', response, '', OPTIONS)
|
||||
const customer = data?.tenant || data?.customer || data
|
||||
const tenantId = id(customer?.id || data?.tenantId)
|
||||
context.assert(MODULE, '海外仓费用隔离客户创建并带 marker', response,
|
||||
Boolean(tenantId) && String(customer?.internalRemark || '').includes(context.marker), `tenantId=${tenantId || 'missing'}`, OPTIONS)
|
||||
if (!tenantId) return null
|
||||
context.createdResources.overseasStorageFeeCustomer = { tenantId, marker: context.marker, ownedByModule: true }
|
||||
return tenantId
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
const start = context.steps.length
|
||||
// Always create a module-owned customer. The customers module may archive its
|
||||
// own fixture, and this lifecycle must never depend on that or on history.
|
||||
const tenantId = await createFixtureCustomer(context)
|
||||
const ownedCustomer = Boolean(tenantId)
|
||||
if (!tenantId) {
|
||||
context.addStep(MODULE, '取得本轮客户 ID', null, false, '未找到可隔离客户,禁止使用历史客户', OPTIONS)
|
||||
return
|
||||
}
|
||||
|
||||
const path = `/api/platform/v1/iam/tenants/${encodeURIComponent(tenantId)}/overseas-storage-fees`
|
||||
const getResponse = await context.request('GET', path)
|
||||
const view = context.apiSuccess(MODULE, '查询客户海外仓费用配置', getResponse, `tenantId=${tenantId}`, OPTIONS)
|
||||
if (!view) {
|
||||
if (ownedCustomer) {
|
||||
const archiveResponse = await context.request('POST', `/api/platform/v1/iam/tenants/${encodeURIComponent(tenantId)}/archive`)
|
||||
const archived = context.apiSuccess(MODULE, '费用配置契约不可用时清理隔离客户', archiveResponse, `tenantId=${tenantId}`, OPTIONS)
|
||||
context.assert(MODULE, '隔离客户清理后停用', archiveResponse, archived?.status === 'DISABLED', `status=${archived?.status ?? 'missing'}`, OPTIONS)
|
||||
}
|
||||
return
|
||||
}
|
||||
const activePath = `/api/platform/v1/iam/tenants/${encodeURIComponent(tenantId)}/overseas-storage-fees`
|
||||
const initial = {
|
||||
id: id(view.id), customerId: id(view.customerId), customerName: view.customerName,
|
||||
currency: view.currency, tiers: requestTiers(view), version: Number(view.version), snapshot: tierSnapshot(view),
|
||||
}
|
||||
context.assert(MODULE, '客户费用配置身份、默认 USD 与9档结构正确', getResponse,
|
||||
initial.id && initial.customerId === tenantId && initial.currency === 'USD' && defaultShape(view),
|
||||
`ruleId=${initial.id || 'missing'}, customerId=${initial.customerId}, currency=${initial.currency}, tiers=${tiersOf(view).length}`, OPTIONS)
|
||||
|
||||
const wrongCustomerResponse = await context.request('PUT', activePath, {
|
||||
customerId: `${BigInt(tenantId) + 1n}`, customerName: initial.customerName, currency: initial.currency,
|
||||
tiers: initial.tiers, version: initial.version,
|
||||
})
|
||||
context.addStep(MODULE, '请求体客户 ID 与路径不一致时安全失败', wrongCustomerResponse,
|
||||
businessFailure(wrongCustomerResponse), `HTTP=${wrongCustomerResponse.http}, code=${codeOf(wrongCustomerResponse)}`, OPTIONS)
|
||||
|
||||
const editedTiers = initial.tiers.map((tier, index) => ({ ...tier, unitPrice: Number((tier.unitPrice + 0.125 + index / 1000).toFixed(3)) }))
|
||||
const updatePayload = {
|
||||
customerId: tenantId, customerName: `${initial.customerName}-${context.marker.slice(-8)}`,
|
||||
currency: 'EUR', tiers: editedTiers, version: initial.version,
|
||||
}
|
||||
const updateResponse = await context.request('PUT', activePath, updatePayload)
|
||||
context.apiSuccess(MODULE, '整组修改客户海外仓费用', updateResponse, `tenantId=${tenantId}, tiers=9`, OPTIONS)
|
||||
const afterUpdateResponse = await context.request('GET', activePath)
|
||||
const afterUpdate = context.apiSuccess(MODULE, '回查整组修改后的客户费用', afterUpdateResponse, '', OPTIONS)
|
||||
const editedCorrect = afterUpdate?.currency === 'EUR' && afterUpdate?.customerName === updatePayload.customerName
|
||||
&& Number(afterUpdate?.version) === initial.version + 1 && tierSnapshot(afterUpdate).every((row, index) => row.join('|') === tierSnapshot({ tiers: editedTiers })[index].join('|'))
|
||||
context.assert(MODULE, '修改后币种、价格、9档及版本整体回显', afterUpdateResponse, editedCorrect,
|
||||
`currency=${afterUpdate?.currency}, version=${afterUpdate?.version}, tiers=${tiersOf(afterUpdate).length}`, OPTIONS)
|
||||
|
||||
const staleResponse = await context.request('PUT', activePath, { ...updatePayload, customerName: `${updatePayload.customerName}-STALE`, version: initial.version })
|
||||
const staleRejected = staleResponse.http === 200 && ['A0409', '409'].includes(codeOf(staleResponse))
|
||||
context.addStep(MODULE, '旧 version 整组保存返回并发冲突', staleResponse, staleRejected,
|
||||
`HTTP=${staleResponse.http}, code=${codeOf(staleResponse)}`, OPTIONS)
|
||||
const afterConflictResponse = await context.request('GET', activePath)
|
||||
const afterConflict = context.apiSuccess(MODULE, '冲突后回查费用配置', afterConflictResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '冲突未造成部分更新', afterConflictResponse,
|
||||
afterConflict?.customerName === updatePayload.customerName && afterConflict?.currency === 'EUR'
|
||||
&& tierSnapshot(afterConflict).every((row, index) => row.join('|') === tierSnapshot({ tiers: editedTiers })[index].join('|')),
|
||||
`version=${afterConflict?.version}, unchanged=${afterConflict?.customerName === updatePayload.customerName}`, OPTIONS)
|
||||
|
||||
const restorePayload = {
|
||||
customerId: tenantId, customerName: initial.customerName, currency: initial.currency,
|
||||
tiers: initial.tiers, version: Number(afterConflict?.version),
|
||||
}
|
||||
const restoreResponse = await context.request('PUT', activePath, restorePayload)
|
||||
context.apiSuccess(MODULE, '恢复本轮客户费用原始配置', restoreResponse, 'restore=initial', OPTIONS)
|
||||
const finalResponse = await context.request('GET', activePath)
|
||||
const final = context.apiSuccess(MODULE, '回查恢复后的客户费用', finalResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '恢复后9档、USD、免费档和无上限档一致', finalResponse,
|
||||
final?.customerName === initial.customerName && final?.currency === 'USD' && defaultShape(final)
|
||||
&& Number(final?.version) === Number(afterConflict?.version) + 1,
|
||||
`currency=${final?.currency}, version=${final?.version}, tiers=${tiersOf(final).length}`, OPTIONS)
|
||||
|
||||
const invalidCustomerResponse = await context.request('GET', '/api/platform/v1/iam/tenants/999999999999999999/overseas-storage-fees')
|
||||
context.addStep(MODULE, '错误客户 ID 查询安全失败', invalidCustomerResponse, businessFailure(invalidCustomerResponse),
|
||||
`HTTP=${invalidCustomerResponse.http}, code=${codeOf(invalidCustomerResponse)}`, OPTIONS)
|
||||
|
||||
if (ownedCustomer) {
|
||||
const archiveResponse = await context.request('POST', `/api/platform/v1/iam/tenants/${encodeURIComponent(tenantId)}/archive`)
|
||||
const archived = context.apiSuccess(MODULE, '清理费用隔离客户', archiveResponse, `tenantId=${tenantId}`, OPTIONS)
|
||||
context.assert(MODULE, '费用隔离客户清理后停用', archiveResponse, archived?.status === 'DISABLED', `status=${archived?.status ?? 'missing'}`, OPTIONS)
|
||||
}
|
||||
const steps = context.steps.slice(start)
|
||||
context.scenarios.push({ name: SCENARIO, module: MODULE, success: steps.every((step) => step.passed), deploymentBlocked: steps.some((step) => step.deploymentBlocker), passed: steps.filter((step) => step.passed).length, failed: steps.filter((step) => !step.passed).length })
|
||||
}
|
||||
Reference in New Issue
Block a user