216 lines
8.4 KiB
JavaScript
216 lines
8.4 KiB
JavaScript
|
|
import assert from 'node:assert/strict'
|
||
|
|
import test from 'node:test'
|
||
|
|
|
||
|
|
import {
|
||
|
|
activateCommercialEntitlement,
|
||
|
|
activateCommercialPlan,
|
||
|
|
activateCommercialSubscription,
|
||
|
|
buildCommercialAnalyticsSearch,
|
||
|
|
buildCommercialPricingScenario,
|
||
|
|
createCommercialPlan,
|
||
|
|
createCommercialSubscription,
|
||
|
|
fetchCommercialAccount,
|
||
|
|
fetchCommercialAnalytics,
|
||
|
|
fetchCommercialCostEvents,
|
||
|
|
fetchCommercialEntitlements,
|
||
|
|
fetchCommercialPlans,
|
||
|
|
fetchCommercialSubscriptions,
|
||
|
|
fetchCommercialUsageEvents,
|
||
|
|
normalizeCommercialPayload,
|
||
|
|
recordCommercialCost,
|
||
|
|
recordCommercialUsage,
|
||
|
|
serializeCommercialPayload,
|
||
|
|
transitionCommercialSubscription,
|
||
|
|
upsertCommercialEntitlement
|
||
|
|
} from '../src/services/commercial.js'
|
||
|
|
|
||
|
|
function installFetch(handler) {
|
||
|
|
const original = globalThis.fetch
|
||
|
|
globalThis.fetch = handler
|
||
|
|
return () => { globalThis.fetch = original }
|
||
|
|
}
|
||
|
|
|
||
|
|
function jsonResponse(payload, status = 200) {
|
||
|
|
return new Response(JSON.stringify(payload), {
|
||
|
|
status,
|
||
|
|
headers: { 'content-type': 'application/json' }
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
test('商业接口递归转换 snake_case 与 camelCase 且保留数组和金额字符串', () => {
|
||
|
|
const normalized = normalizeCommercialPayload({
|
||
|
|
data_status: 'partial',
|
||
|
|
customer_roi: { required_inputs: ['收费'], ratios: [{ reporting_currency: 'CNY', ratio: '1.20' }] }
|
||
|
|
})
|
||
|
|
assert.equal(normalized.dataStatus, 'partial')
|
||
|
|
assert.equal(normalized.customerRoi.requiredInputs[0], '收费')
|
||
|
|
assert.equal(normalized.customerRoi.ratios[0].reportingCurrency, 'CNY')
|
||
|
|
assert.equal(normalized.customerRoi.ratios[0].ratio, '1.20')
|
||
|
|
|
||
|
|
assert.deepEqual(serializeCommercialPayload({
|
||
|
|
planCode: 'enterprise',
|
||
|
|
configJson: { billingMode: 'overage' },
|
||
|
|
requiredInputs: ['invoice']
|
||
|
|
}), {
|
||
|
|
plan_code: 'enterprise',
|
||
|
|
config_json: { billing_mode: 'overage' },
|
||
|
|
required_inputs: ['invoice']
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
test('普通账号只读取自身账户,平台管理员显式选择租户后才使用跨租户路径', async () => {
|
||
|
|
const urls = []
|
||
|
|
const restore = installFetch(async (url) => {
|
||
|
|
urls.push(String(url))
|
||
|
|
return jsonResponse({ tenant_id: 'tenant-a', data_status: 'unavailable', quotas: [] })
|
||
|
|
})
|
||
|
|
try {
|
||
|
|
await fetchCommercialAccount({ platformAdmin: false, tenantId: 'should-not-leak' })
|
||
|
|
await fetchCommercialAccount({ platformAdmin: true, tenantId: 'tenant/a', asOf: '2026-07-16T00:00:00Z' })
|
||
|
|
} finally {
|
||
|
|
restore()
|
||
|
|
}
|
||
|
|
|
||
|
|
assert.equal(urls[0], '/api/v1/commercial/account')
|
||
|
|
assert.match(urls[1], /\/commercial\/admin\/tenants\/tenant%2Fa\/account\?as_of=2026-07-16T00%3A00%3A00Z$/u)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('商业分析时间参数显式序列化且不附加客户端汇率或合计字段', async () => {
|
||
|
|
const search = buildCommercialAnalyticsSearch({
|
||
|
|
start: '2026-04-01T00:00:00Z',
|
||
|
|
end: '2026-07-01T00:00:00Z',
|
||
|
|
asOf: '2026-07-01T00:00:00Z',
|
||
|
|
fxRate: '7.2'
|
||
|
|
})
|
||
|
|
assert.equal(search.get('as_of'), '2026-07-01T00:00:00Z')
|
||
|
|
assert.equal(search.has('fx_rate'), false)
|
||
|
|
|
||
|
|
let requestedUrl = ''
|
||
|
|
const restore = installFetch(async (url) => {
|
||
|
|
requestedUrl = String(url)
|
||
|
|
return jsonResponse({
|
||
|
|
tenant_id: 'tenant-a',
|
||
|
|
data_quality_status: 'unavailable',
|
||
|
|
customer_charges: {},
|
||
|
|
internal_costs: {},
|
||
|
|
contribution_margin: {},
|
||
|
|
verified_cash_savings: {},
|
||
|
|
customer_roi: {},
|
||
|
|
customer_labor_value: {}
|
||
|
|
})
|
||
|
|
})
|
||
|
|
try {
|
||
|
|
await fetchCommercialAnalytics('tenant-a', Object.fromEntries(search))
|
||
|
|
} finally {
|
||
|
|
restore()
|
||
|
|
}
|
||
|
|
assert.match(requestedUrl, /\/commercial\/admin\/tenants\/tenant-a\/analytics/u)
|
||
|
|
assert.match(requestedUrl, /start=2026-04-01T00%3A00%3A00Z/u)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('定价场景使用平台租户 POST 契约且只提交服务端 schema 字段', async () => {
|
||
|
|
let request = null
|
||
|
|
const restore = installFetch(async (url, options = {}) => {
|
||
|
|
request = { url: String(url), method: options.method, body: JSON.parse(options.body) }
|
||
|
|
return jsonResponse({
|
||
|
|
tenant_id: 'tenant-a',
|
||
|
|
recommended_model: 'pilot_collecting',
|
||
|
|
evidence_status: 'unavailable',
|
||
|
|
scenarios: [],
|
||
|
|
notes: ['不会自动修改套餐']
|
||
|
|
})
|
||
|
|
})
|
||
|
|
try {
|
||
|
|
const response = await buildCommercialPricingScenario('tenant/a', {
|
||
|
|
start: '2026-04-01T00:00:00Z',
|
||
|
|
end: '2026-07-01T23:59:59Z',
|
||
|
|
asOf: '2026-07-02T00:00:00Z',
|
||
|
|
targetContributionMarginRate: '0.65',
|
||
|
|
maxVerifiedSavingsShare: '0.25'
|
||
|
|
})
|
||
|
|
assert.equal(response.recommendedModel, 'pilot_collecting')
|
||
|
|
} finally {
|
||
|
|
restore()
|
||
|
|
}
|
||
|
|
|
||
|
|
assert.equal(request.method, 'POST')
|
||
|
|
assert.match(request.url, /\/commercial\/admin\/tenants\/tenant%2Fa\/pricing-scenarios$/u)
|
||
|
|
assert.deepEqual(Object.keys(request.body).sort(), [
|
||
|
|
'as_of',
|
||
|
|
'end',
|
||
|
|
'max_verified_savings_share',
|
||
|
|
'start',
|
||
|
|
'target_contribution_margin_rate'
|
||
|
|
])
|
||
|
|
assert.equal(request.body.target_contribution_margin_rate, '0.65')
|
||
|
|
assert.equal(request.body.max_verified_savings_share, '0.25')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('商业写接口覆盖套餐、订阅、权益、激活、用量与成本并统一递归序列化', async () => {
|
||
|
|
const requests = []
|
||
|
|
const restore = installFetch(async (url, options = {}) => {
|
||
|
|
requests.push({ url: String(url), method: options.method, headers: options.headers, body: JSON.parse(options.body) })
|
||
|
|
return jsonResponse({ id: 'resource-1', created: true })
|
||
|
|
})
|
||
|
|
try {
|
||
|
|
await createCommercialPlan('tenant-a', { planCode: 'enterprise', contractTermsJson: { approvedBy: 'cfo' } })
|
||
|
|
await activateCommercialPlan('tenant-a', 'plan-1', 2, '启用新套餐版本')
|
||
|
|
await createCommercialSubscription('tenant-a', { subscriptionKey: 'sub-1', autoRenew: true })
|
||
|
|
await activateCommercialSubscription('tenant-a', 'sub-1', 3, '恢复客户订阅')
|
||
|
|
await transitionCommercialSubscription('tenant-a', 'sub-1', { expectedVersion: 4, targetStatus: 'suspended', reason: '合同暂停' })
|
||
|
|
await upsertCommercialEntitlement('tenant-a', { entitlementKey: 'ocr', configJson: { billingMode: 'all_usage' } })
|
||
|
|
await activateCommercialEntitlement('tenant-a', 'ent-1', 4, '恢复 OCR 权益')
|
||
|
|
await recordCommercialUsage('tenant-a', { idempotencyKey: 'usage-1', subjectType: 'claim' })
|
||
|
|
await recordCommercialCost('tenant-a', { idempotencyKey: 'cost-1', originalCurrency: 'CNY' })
|
||
|
|
} finally {
|
||
|
|
restore()
|
||
|
|
}
|
||
|
|
|
||
|
|
assert.deepEqual(requests.map((item) => item.method), ['POST', 'POST', 'POST', 'POST', 'POST', 'PUT', 'POST', 'POST', 'POST'])
|
||
|
|
assert.match(requests[0].url, /\/plans$/u)
|
||
|
|
assert.equal(requests[0].body.contract_terms_json.approved_by, 'cfo')
|
||
|
|
assert.equal(requests[1].body.expected_version, 2)
|
||
|
|
assert.equal(requests[1].body.reason, '启用新套餐版本')
|
||
|
|
assert.ok(requests.every((item) => item.headers['X-Request-Id']))
|
||
|
|
assert.equal(requests[2].body.auto_renew, true)
|
||
|
|
assert.match(requests[4].url, /\/subscriptions\/sub-1\/transition$/u)
|
||
|
|
assert.equal(requests[4].body.target_status, 'suspended')
|
||
|
|
assert.equal(requests[5].body.config_json.billing_mode, 'all_usage')
|
||
|
|
assert.match(requests[7].url, /\/usage-events$/u)
|
||
|
|
assert.match(requests[8].url, /\/cost-events$/u)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('五类商业历史按最终分资源契约读取并正确序列化筛选', async () => {
|
||
|
|
const urls = []
|
||
|
|
const restore = installFetch(async (url) => {
|
||
|
|
urls.push(String(url))
|
||
|
|
return jsonResponse([])
|
||
|
|
})
|
||
|
|
try {
|
||
|
|
await fetchCommercialPlans('tenant-a', { planStatus: 'active', limit: 20, offset: 1 })
|
||
|
|
await fetchCommercialSubscriptions('tenant-a', { subscriptionStatus: 'suspended' })
|
||
|
|
await fetchCommercialEntitlements('tenant-a', { subscriptionId: 'sub-1', entitlementStatus: 'active' })
|
||
|
|
await fetchCommercialUsageEvents('tenant-a', { subscriptionId: 'sub-1', start: '2026-07-01T00:00:00Z', end: '2026-08-01T00:00:00Z' })
|
||
|
|
await fetchCommercialCostEvents('tenant-a', { limit: 50 })
|
||
|
|
} finally {
|
||
|
|
restore()
|
||
|
|
}
|
||
|
|
assert.match(urls[0], /\/plans\?plan_status=active&limit=20&offset=1$/u)
|
||
|
|
assert.match(urls[1], /\/subscriptions\?subscription_status=suspended$/u)
|
||
|
|
assert.match(urls[2], /\/entitlements\?subscription_id=sub-1&entitlement_status=active$/u)
|
||
|
|
assert.match(urls[3], /\/usage-events\?subscription_id=sub-1&start=.*&end=.*/u)
|
||
|
|
assert.match(urls[4], /\/cost-events\?limit=50$/u)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('后端权限和冲突错误保留状态与可执行提示', async () => {
|
||
|
|
const restore = installFetch(async () => jsonResponse({ detail: '只有平台管理员可以执行该操作。' }, 403))
|
||
|
|
try {
|
||
|
|
await assert.rejects(
|
||
|
|
() => fetchCommercialAnalytics('tenant-a'),
|
||
|
|
(error) => error.status === 403 && /平台管理员/u.test(error.message)
|
||
|
|
)
|
||
|
|
} finally {
|
||
|
|
restore()
|
||
|
|
}
|
||
|
|
})
|