优化
This commit is contained in:
@@ -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