Merge remote-tracking branch 'origin/main' into codex/usage-pending-reasoning-reset-expiry-20260712

# Conflicts:
#	frontend/src/features/usage/components/__tests__/UsageRecordsTable.spec.ts
This commit is contained in:
MMEXA
2026-07-17 00:16:58 +08:00
40 changed files with 5012 additions and 565 deletions
@@ -250,11 +250,12 @@
</span>
</div>
<ServiceTierFacts
v-if="hasServiceTierFacts"
v-if="hasServiceTierFacts || processingTierPriceMultiplier !== null"
class="mt-3"
:requested="serviceTierFacts.requested"
:actual="serviceTierFacts.actual"
:billing="serviceTierFacts.billing"
:price-multiplier="processingTierPriceMultiplier"
/>
</div>
@@ -313,13 +314,13 @@
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-muted-foreground sm:hidden">
<div class="grid grid-cols-[max-content_1fr] items-baseline gap-x-1">
<span>输入</span>
<span class="text-right">${{ formatPrice(tier.input_price_per_1m) }}/M</span>
<span class="text-right">{{ formatPricePerMillion(tier.input_price_per_1m) }}</span>
</div>
<div
class="grid grid-cols-[max-content_1fr] items-baseline gap-x-1"
>
<span>输出</span>
<span class="text-right">${{ formatPrice(tier.output_price_per_1m) }}/M</span>
<span class="text-right">{{ formatPricePerMillion(tier.output_price_per_1m) }}</span>
</div>
<template v-if="getTierActiveCacheCreationDisplay(tier) || shouldShowCacheReadPrice(tier)">
<div
@@ -341,8 +342,8 @@
</template>
</div>
<div class="text-muted-foreground hidden items-center gap-2 flex-wrap sm:flex">
<span>输入 ${{ formatPrice(tier.input_price_per_1m) }}/M</span>
<span>输出 ${{ formatPrice(tier.output_price_per_1m) }}/M</span>
<span>输入 {{ formatPricePerMillion(tier.input_price_per_1m) }}</span>
<span>输出 {{ formatPricePerMillion(tier.output_price_per_1m) }}</span>
<span v-if="getTierActiveCacheCreationDisplay(tier)">
{{ getTierActiveCacheCreationDisplay(tier)?.label }}
${{ formatPrice(getTierActiveCacheCreationDisplay(tier)?.price || 0) }}/M
@@ -906,6 +907,12 @@ import {
resolveUsageStreamLabelSegments,
} from '../utils/status'
import { resolveRequestFailureNotice } from '../utils/errorNotice'
import {
formatPricePerMillion,
resolveProcessingTierPriceMultiplier,
resolveSettlementPricingSourceLabel,
resolveSettlementPricingTiers,
} from '../utils/settlement-pricing'
// 子组件
import RequestHeadersContent from './RequestDetailDrawer/RequestHeadersContent.vue'
@@ -1355,6 +1362,9 @@ const failureNotice = computed(() => resolveRequestFailureNotice(detail.value))
const serviceTierFacts = computed(() => resolveServiceTierFacts(detail.value))
const hasServiceTierFacts = computed(() => hasServiceTierFact(serviceTierFacts.value))
const processingTierPriceMultiplier = computed(() => (
resolveProcessingTierPriceMultiplier(detail.value)
))
const settlementInfo = computed<JsonRecord | null>(() =>
asRecord(detail.value?.settlement ?? null),
@@ -1643,20 +1653,10 @@ const hasValidConversation = computed(() => {
return false
})
// 价格来源标签
// tiered_pricing.source 表示定价来源: 'provider' 或 'global'
// 价格来源优先使用 v3 结算快照,旧 tiered_pricing.source 仅作回退。
const priceSourceLabel = computed(() => {
if (!detail.value) return '历史定价'
const source = detail.value.tiered_pricing?.source
if (source === 'provider') {
return '提供商定价'
} else if (source === 'global') {
return '全局定价'
}
// 没有 tiered_pricing 时,使用历史价格
return '历史定价'
return resolveSettlementPricingSourceLabel(detail.value) ?? '历史定价'
})
const cacheCreationInputTokens5m = computed(() => {
@@ -1896,10 +1896,9 @@ const activeCacheTtlMinutes = computed(() => {
const displayTiers = computed(() => {
if (!detail.value) return []
// 如果有阶梯定价数据,直接使用
if (detail.value.tiered_pricing?.tiers && detail.value.tiered_pricing.tiers.length > 0) {
return detail.value.tiered_pricing.tiers
}
// 优先展示结算快照中已解析的价格目录,旧字段仅作回退。
const resolvedTiers = resolveSettlementPricingTiers(detail.value)
if (resolvedTiers) return resolvedTiers as PricingTierLike[]
// 否则用历史价格构建单阶梯(无上限)
return [{
@@ -1,6 +1,7 @@
<template>
<dl
class="grid grid-cols-1 gap-x-4 gap-y-1.5 text-xs sm:grid-cols-3"
class="grid grid-cols-1 gap-x-4 gap-y-1.5 text-xs"
:class="hasPriceMultiplier ? 'sm:grid-cols-4' : 'sm:grid-cols-3'"
data-testid="service-tier-facts"
>
<div class="flex min-w-0 items-baseline justify-between gap-3 sm:block">
@@ -9,9 +10,9 @@
</dt>
<dd
class="truncate font-mono font-medium text-foreground sm:mt-0.5"
:title="requested || '-'"
:title="formatServiceTierFact(requested) || '-'"
>
{{ requested || '-' }}
{{ formatServiceTierFact(requested) || '-' }}
</dd>
</div>
<div class="flex min-w-0 items-baseline justify-between gap-3 sm:block">
@@ -20,9 +21,9 @@
</dt>
<dd
class="truncate font-mono font-medium text-foreground sm:mt-0.5"
:title="actual || '-'"
:title="formatServiceTierFact(actual) || '-'"
>
{{ actual || '-' }}
{{ formatServiceTierFact(actual) || '-' }}
</dd>
</div>
<div class="flex min-w-0 items-baseline justify-between gap-3 sm:block">
@@ -31,18 +32,48 @@
</dt>
<dd
class="truncate font-mono font-medium text-foreground sm:mt-0.5"
:title="billing || '-'"
:title="formatServiceTierFact(billing) || '-'"
>
{{ billing || '-' }}
{{ formatServiceTierFact(billing) || '-' }}
</dd>
</div>
<div
v-if="hasPriceMultiplier"
class="flex min-w-0 items-baseline justify-between gap-3 sm:block"
data-testid="service-tier-price-multiplier"
>
<dt class="text-muted-foreground">
{{ multiplierTierLabel }} 倍率
</dt>
<dd class="truncate font-mono font-medium text-foreground sm:mt-0.5">
{{ formattedPriceMultiplier }}×
</dd>
</div>
</dl>
</template>
<script setup lang="ts">
defineProps<{
import { computed } from 'vue'
import { formatServiceTierFact } from '../utils/service-tier'
const props = defineProps<{
requested: string | null
actual: string | null
billing: string | null
priceMultiplier?: number | null
}>()
const hasPriceMultiplier = computed(() => (
typeof props.priceMultiplier === 'number'
&& Number.isFinite(props.priceMultiplier)
&& props.priceMultiplier >= 0
))
const multiplierTierLabel = computed(() => (
formatServiceTierFact(props.billing ?? props.actual ?? props.requested) ?? '处理层级'
))
const formattedPriceMultiplier = computed(() => (
hasPriceMultiplier.value ? String(props.priceMultiplier) : ''
))
</script>
@@ -1129,6 +1129,7 @@ import { useRowClick } from '@/composables/useRowClick'
import { useDarkMode } from '@/composables/useDarkMode'
import { API_FORMAT_ORDER, formatApiFormat } from '@/api/endpoints/types/api-format'
import { formatClientFamily } from '@/features/usage/utils/clientFamily'
import { formatServiceTierFact } from '../utils/service-tier'
import type { DateRangeParams, UsageRecord } from '../types'
import { MultiSelect, TimeRangePicker } from '@/components/common'
import type { MultiSelectOption } from '@/components/common/MultiSelect.vue'
@@ -1663,6 +1664,9 @@ function canonicalServiceTier(value: string | null): string | null {
if (value === 'auto' || value === 'default' || value === 'standard') {
return 'standard'
}
if (value === 'fast') {
return 'priority'
}
return value
}
@@ -1689,10 +1693,13 @@ function buildServiceTierBadgePresentation(
billingTier: string | null,
): ServiceTierBadgePresentation {
const titleLines: string[] = []
if (requestedRaw) titleLines.push(`请求档位:${requestedRaw}`)
if (actualRaw) titleLines.push(`实际档位:${actualRaw}`)
if (billingTier) {
titleLines.push(`计费档位:${billingTier}`)
const requestedLabel = formatServiceTierFact(requestedRaw)
const actualLabel = formatServiceTierFact(actualRaw)
const billingLabel = formatServiceTierFact(billingTier)
if (requestedLabel) titleLines.push(`请求档位:${requestedLabel}`)
if (actualLabel) titleLines.push(`实际档位:${actualLabel}`)
if (billingLabel) {
titleLines.push(`计费档位:${billingLabel}`)
} else {
titleLines.push(`计费档位:${state === 'pending' ? '待上游确认' : '未确认'}`)
}
@@ -1717,7 +1724,7 @@ function getServiceTierBadge(record: UsageRecord): ServiceTierBadgePresentation
if (actual) {
if (requestedFast && !actualFast) {
return buildServiceTierBadgePresentation(
`fast → ${actual}`,
`Fast → ${actual}`,
'downgraded',
requestedRaw,
actualRaw,
@@ -1727,7 +1734,7 @@ function getServiceTierBadge(record: UsageRecord): ServiceTierBadgePresentation
if (!requestedFast && actualFast) {
const requestedLabel = requested ?? 'standard'
return buildServiceTierBadgePresentation(
requested ? `${requestedLabel} → fast` : 'fast',
requested ? `${requestedLabel} → Fast` : 'Fast',
requested ? 'upgraded' : 'confirmed',
requestedRaw,
actualRaw,
@@ -1736,7 +1743,7 @@ function getServiceTierBadge(record: UsageRecord): ServiceTierBadgePresentation
}
if (actualFast) {
return buildServiceTierBadgePresentation(
'fast',
'Fast',
'confirmed',
requestedRaw,
actualRaw,
@@ -1750,7 +1757,7 @@ function getServiceTierBadge(record: UsageRecord): ServiceTierBadgePresentation
const displayStatus = getDisplayStatus(record)
const isActive = displayStatus === 'pending' || displayStatus === 'streaming'
return buildServiceTierBadgePresentation(
isActive ? 'fast · 待确认' : 'fast · 未确认',
isActive ? 'Fast · 待确认' : 'Fast · 未确认',
isActive ? 'pending' : 'unconfirmed',
requestedRaw,
null,
@@ -1762,8 +1769,8 @@ function getServiceTierTitle(record: UsageRecord): string {
const badge = getServiceTierBadge(record)
if (badge) return badge.title
const requested = normalizeServiceTier(record.service_tier)
const actual = normalizeServiceTier(record.actual_service_tier)
const requested = formatServiceTierFact(record.service_tier)
const actual = formatServiceTierFact(record.actual_service_tier)
return [
requested ? `请求档位:${requested}` : null,
actual ? `实际档位:${actual}` : null,
@@ -0,0 +1,100 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createApp, defineComponent, h, nextTick, ref, type App, type Ref } from 'vue'
import type { RequestDetail } from '@/api/dashboard'
import RequestDetailDrawer from '../RequestDetailDrawer.vue'
const apiMocks = vi.hoisted(() => ({
getRequestDetail: vi.fn(),
}))
vi.mock('@/api/dashboard', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/api/dashboard')>()
return {
...actual,
dashboardApi: {
...actual.dashboardApi,
getRequestDetail: apiMocks.getRequestDetail,
},
}
})
const mountedApps: Array<{ app: App, root: HTMLElement }> = []
afterEach(() => {
for (const { app, root } of mountedApps.splice(0)) {
app.unmount()
root.remove()
}
apiMocks.getRequestDetail.mockReset()
})
function buildEmbeddingDetail(): RequestDetail {
return {
id: 'usage-embedding-1',
request_id: 'req-embedding-1',
user: {
id: 'user-1',
username: 'embedding-user',
email: '[email protected]',
},
api_key: {
id: 'key-1',
name: 'test-key',
display: 'test-key',
},
provider: 'embedding-provider',
model: 'embedding-model',
tokens: { input: 100, output: 0, total: 100 },
cost: { input: 0.00001, output: 0, total: 0.00001 },
request_type: 'embedding',
is_stream: false,
status: 'completed',
status_code: 200,
response_time_ms: 10,
created_at: '2026-07-16T00:00:00Z',
request_headers: { 'content-type': 'application/json' },
settlement: {
settlement_snapshot: {
pricing_snapshot: {
pricing_source: 'global_default',
tiered_pricing: {
tiers: [{ up_to: null, input_price_per_1m: 0.1 }],
},
},
},
},
}
}
describe('RequestDetailDrawer settlement pricing', () => {
it('renders an input-only embedding tier without treating the missing output price as zero', async () => {
apiMocks.getRequestDetail.mockResolvedValue(buildEmbeddingDetail())
let isOpen!: Ref<boolean>
const Host = defineComponent({
setup() {
isOpen = ref(false)
return () => h(RequestDetailDrawer, {
isOpen: isOpen.value,
requestId: 'usage-embedding-1',
})
},
})
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp(Host)
app.mount(root)
mountedApps.push({ app, root })
isOpen.value = true
await nextTick()
await vi.waitFor(() => {
expect(document.body.textContent).toContain('输入 $0.1/M')
expect(document.body.textContent).toContain('输出 -')
})
expect(document.body.textContent).not.toContain('输出 $0/M')
})
})
@@ -33,9 +33,75 @@ describe('ServiceTierFacts', () => {
'计费层级',
])
expect([...root.querySelectorAll('dd')].map(node => node.textContent?.trim())).toEqual([
'priority',
'Fast',
'-',
'flex',
])
expect([...root.querySelectorAll('dd')].map(node => node.getAttribute('title'))).toEqual([
'Fast',
'-',
'flex',
])
})
it('uses the same Fast label for raw priority and fast facts', () => {
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp({
render: () => h(ServiceTierFacts, {
requested: 'priority',
actual: 'fast',
billing: 'priority',
}),
})
app.mount(root)
mountedApps.push({ app, root })
expect([...root.querySelectorAll('dd')].map(node => node.textContent?.trim())).toEqual([
'Fast',
'Fast',
'Fast',
])
expect([...root.querySelectorAll('dd')].map(node => node.getAttribute('title'))).toEqual([
'Fast',
'Fast',
'Fast',
])
})
it('renders the processing-tier multiplier with the billing tier label', () => {
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp({
render: () => h(ServiceTierFacts, {
requested: 'priority',
actual: 'fast',
billing: 'fast',
priceMultiplier: 2.5,
}),
})
app.mount(root)
mountedApps.push({ app, root })
const multiplier = root.querySelector('[data-testid="service-tier-price-multiplier"]')
expect(multiplier?.textContent).toContain('Fast 倍率')
expect(multiplier?.textContent).toContain('2.5×')
})
it('does not render an empty or invalid processing-tier multiplier', () => {
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp({
render: () => h(ServiceTierFacts, {
requested: 'priority',
actual: null,
billing: null,
priceMultiplier: null,
}),
})
app.mount(root)
mountedApps.push({ app, root })
expect(root.querySelector('[data-testid="service-tier-price-multiplier"]')).toBeNull()
})
})
@@ -162,11 +162,12 @@ function mountUsageRecordsTable(records: UsageRecord[], overrides: Record<string
return root
}
function expectServiceTierBadge(root: HTMLElement, label: string) {
const labels = [...root.querySelectorAll('span')]
.map((element) => element.textContent?.trim())
function expectServiceTierBadge(root: HTMLElement, label: string): HTMLElement {
const badge = [...root.querySelectorAll<HTMLElement>('span')]
.find(element => element.textContent?.trim() === label)
expect(labels).toContain(label)
expect(badge).toBeDefined()
return badge as HTMLElement
}
afterEach(() => {
@@ -313,13 +314,26 @@ describe('UsageRecordsTable', () => {
expect(root.textContent).toContain('会话压缩')
})
it('shows confirmed fast when requested and actual service tiers are priority', () => {
it.each([
['priority', 'priority'],
['fast', 'fast'],
['priority', 'fast'],
['fast', 'priority'],
])('shows confirmed Fast for requested %s and actual %s', (requested, actual) => {
const root = mountUsageRecordsTable([buildRecord({
service_tier: 'priority',
actual_service_tier: 'priority',
service_tier: requested,
actual_service_tier: actual,
})])
expectServiceTierBadge(root, 'fast')
const badge = expectServiceTierBadge(root, 'Fast')
expect(badge.getAttribute('title')).toBe([
'请求档位:Fast',
'实际档位:Fast',
'计费档位:Fast',
].join('\n'))
expect(badge.getAttribute('aria-label')).toBe(
'请求档位:Fast,实际档位:Fast,计费档位:Fast',
)
})
it('shows fast to standard when the provider downgrades a priority request', () => {
@@ -328,7 +342,12 @@ describe('UsageRecordsTable', () => {
actual_service_tier: 'default',
})])
expectServiceTierBadge(root, 'fast → standard')
const badge = expectServiceTierBadge(root, 'Fast → standard')
expect(badge.getAttribute('title')).toBe([
'请求档位:Fast',
'实际档位:default',
'计费档位:standard',
].join('\n'))
})
it('shows fast to flex when the provider moves a priority request to flex', () => {
@@ -337,7 +356,7 @@ describe('UsageRecordsTable', () => {
actual_service_tier: 'flex',
})])
expectServiceTierBadge(root, 'fast → flex')
expectServiceTierBadge(root, 'Fast → flex')
})
it('shows standard to fast when the provider upgrades a default request', () => {
@@ -346,7 +365,7 @@ describe('UsageRecordsTable', () => {
actual_service_tier: 'priority',
})])
expectServiceTierBadge(root, 'standard → fast')
expectServiceTierBadge(root, 'standard → Fast')
})
it.each(['pending', 'streaming'] as const)(
@@ -358,7 +377,7 @@ describe('UsageRecordsTable', () => {
status,
})])
expectServiceTierBadge(root, 'fast · 待确认')
expectServiceTierBadge(root, 'Fast · 待确认')
},
)
@@ -369,7 +388,7 @@ describe('UsageRecordsTable', () => {
status: 'completed',
})])
expectServiceTierBadge(root, 'fast · 未确认')
expectServiceTierBadge(root, 'Fast · 未确认')
})
it('offers embedding API formats in the usage record filter', () => {
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
formatServiceTierFact,
hasServiceTierFact,
normalizeServiceTierFact,
resolveServiceTierFacts,
@@ -38,4 +39,16 @@ describe('service tier facts', () => {
expect(normalizeServiceTierFact(' ')).toBeNull()
expect(normalizeServiceTierFact(0)).toBeNull()
})
it.each(['priority', 'fast', ' Priority ', 'FAST'])(
'displays the raw %s tier as Fast',
(tier) => {
expect(formatServiceTierFact(tier)).toBe('Fast')
},
)
it('keeps non-fast tier labels unchanged', () => {
expect(formatServiceTierFact(' Batch ')).toBe('Batch')
expect(formatServiceTierFact(' ')).toBeNull()
})
})
@@ -0,0 +1,94 @@
import { describe, expect, it } from 'vitest'
import {
formatPricePerMillion,
resolveProcessingTierPriceMultiplier,
resolveSettlementPricingSnapshot,
resolveSettlementPricingSourceLabel,
resolveSettlementPricingTiers,
} from '../settlement-pricing'
function buildSource(overrides: Record<string, unknown> = {}) {
return {
settlement: {
rate_multiplier: 9,
settlement_snapshot: {
pricing_snapshot: {
billing_processing_tier: 'priority',
pricing_source: 'provider_override',
tiered_pricing_source: 'global_default',
processing_tier_price_multiplier: 2.5,
tiered_pricing: {
tiers: [{ up_to: null, input_price_per_1m: 5, output_price_per_1m: 30 }],
},
...overrides,
},
},
},
tiered_pricing: {
source: 'global',
tiers: [{ up_to: null, input_price_per_1m: 1 }],
},
}
}
describe('settlement pricing presentation', () => {
it('reads the resolved pricing snapshot and prefers its catalog over legacy tiers', () => {
const source = buildSource()
expect(resolveSettlementPricingSnapshot(source)?.billing_processing_tier).toBe('priority')
expect(resolveSettlementPricingTiers(source)).toEqual([
{ up_to: null, input_price_per_1m: 5, output_price_per_1m: 30 },
])
})
it.each([
['provider_override', '提供商定价'],
['global_default', '全局定价'],
['mixed', '混合定价'],
])('maps the resolved %s pricing source', (pricingSource, label) => {
expect(resolveSettlementPricingSourceLabel(buildSource({
pricing_source: pricingSource,
}))).toBe(label)
})
it('falls back from pricing_source to tiered_pricing_source and then legacy source', () => {
expect(resolveSettlementPricingSourceLabel(buildSource({
pricing_source: null,
tiered_pricing_source: 'global_default',
}))).toBe('全局定价')
expect(resolveSettlementPricingSourceLabel({
tiered_pricing: { source: 'provider' },
})).toBe('提供商定价')
})
it('uses only processing_tier_price_multiplier, never settlement.rate_multiplier', () => {
expect(resolveProcessingTierPriceMultiplier(buildSource())).toBe(2.5)
expect(resolveProcessingTierPriceMultiplier(buildSource({
processing_tier_price_multiplier: null,
}))).toBeNull()
expect(resolveProcessingTierPriceMultiplier({
settlement: { rate_multiplier: 9 },
})).toBeNull()
})
it('falls back to legacy tiers when the resolved snapshot has no catalog', () => {
expect(resolveSettlementPricingTiers(buildSource({ tiered_pricing: null }))).toEqual([
{ up_to: null, input_price_per_1m: 1 },
])
})
it('formats an input-only embedding tier without inventing an output price', () => {
const tiers = resolveSettlementPricingTiers(buildSource({
tiered_pricing: {
tiers: [{ up_to: null, input_price_per_1m: 0.1 }],
},
}))
expect(formatPricePerMillion(tiers?.[0]?.input_price_per_1m)).toBe('$0.1/M')
expect(formatPricePerMillion(tiers?.[0]?.output_price_per_1m)).toBe('-')
expect(formatPricePerMillion(null)).toBe('-')
expect(formatPricePerMillion(0)).toBe('$0/M')
})
})
@@ -33,6 +33,21 @@ export function normalizeServiceTierFact(value: unknown): string | null {
return normalized || null
}
/**
* Provider contracts use both `priority` (OpenAI) and `fast` (Claude) for the
* same user-facing processing mode. Keep the raw fact in data structures and
* normalize only at the presentation boundary.
*/
export function formatServiceTierFact(value: unknown): string | null {
const normalized = normalizeServiceTierFact(value)
if (normalized === null) return null
const canonical = normalized.toLowerCase()
return canonical === 'priority' || canonical === 'fast'
? 'Fast'
: normalized
}
function asRecord(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
@@ -0,0 +1,85 @@
interface SettlementPricingSource {
settlement?: unknown
tiered_pricing?: unknown
}
type JsonRecord = Record<string, unknown>
const PRICING_SOURCE_LABELS: Record<string, string> = {
provider_override: '提供商定价',
global_default: '全局定价',
mixed: '混合定价',
provider: '提供商定价',
global: '全局定价',
unpriced: '未定价',
}
export function resolveSettlementPricingSnapshot(
source: SettlementPricingSource | null | undefined,
): JsonRecord | null {
const settlement = asRecord(source?.settlement)
const settlementSnapshot = asRecord(settlement?.settlement_snapshot)
return asRecord(settlementSnapshot?.pricing_snapshot)
}
export function resolveSettlementPricingSourceLabel(
source: SettlementPricingSource | null | undefined,
): string | null {
const snapshot = resolveSettlementPricingSnapshot(source)
const legacyPricing = asRecord(source?.tiered_pricing)
for (const value of [
snapshot?.pricing_source,
snapshot?.tiered_pricing_source,
legacyPricing?.source,
]) {
const key = normalizeString(value)?.toLowerCase()
if (key && PRICING_SOURCE_LABELS[key]) return PRICING_SOURCE_LABELS[key]
}
return null
}
export function resolveSettlementPricingTiers(
source: SettlementPricingSource | null | undefined,
): JsonRecord[] | null {
const snapshot = resolveSettlementPricingSnapshot(source)
const snapshotPricing = asRecord(snapshot?.tiered_pricing)
const snapshotTiers = nonEmptyRecordArray(snapshotPricing?.tiers)
if (snapshotTiers) return snapshotTiers
const legacyPricing = asRecord(source?.tiered_pricing)
return nonEmptyRecordArray(legacyPricing?.tiers)
}
export function resolveProcessingTierPriceMultiplier(
source: SettlementPricingSource | null | undefined,
): number | null {
const value = resolveSettlementPricingSnapshot(source)?.processing_tier_price_multiplier
return typeof value === 'number' && Number.isFinite(value) && value >= 0
? value
: null
}
export function formatPricePerMillion(value: unknown): string {
if (typeof value !== 'number' || !Number.isFinite(value)) return '-'
const fixed = value.toFixed(4)
return `$${Number.parseFloat(fixed).toString()}/M`
}
function normalizeString(value: unknown): string | null {
if (typeof value !== 'string') return null
const normalized = value.trim()
return normalized || null
}
function nonEmptyRecordArray(value: unknown): JsonRecord[] | null {
if (!Array.isArray(value) || value.length === 0) return null
const records = value.filter((item): item is JsonRecord => asRecord(item) !== null)
return records.length > 0 ? records : null
}
function asRecord(value: unknown): JsonRecord | null {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? value as JsonRecord
: null
}