Files
aiturk-hermes-ide/apps/desktop/src/app/settings/billing/billing-amounts.ts
T

127 lines
3.4 KiB
TypeScript

import { type BillingCopy, DEFAULT_BILLING_COPY } from './copy'
import type { BillingStateResponse } from './types'
import { EMPTY_BILLING_VALUE } from './use-billing-state'
export function clampAmount(raw: string, billing: Pick<BillingStateResponse, 'max_usd' | 'min_usd'>): string {
const amount = parseAmount(raw)
if (amount == null) {
return ''
}
const min = parseAmount(billing.min_usd)
const max = parseAmount(billing.max_usd)
const clampedMin = min == null ? amount : Math.max(min, amount)
const clamped = max == null ? clampedMin : Math.min(max, clampedMin)
return formatAmountForRequest(clamped)
}
export function parseAmount(value?: null | number | string): null | number {
if (typeof value === 'number') {
return Number.isFinite(value) ? value : null
}
if (typeof value !== 'string') {
return null
}
const parsed = Number(value.replace(/[$,\s]/g, ''))
return Number.isFinite(parsed) && parsed > 0 ? parsed : null
}
export function formatAmountForRequest(value: number): string {
return Number.isInteger(value) ? String(value) : value.toFixed(2).replace(/0+$/, '').replace(/\.$/, '')
}
export function initialAutoReloadAmount(...candidates: Array<null | string | undefined>): string {
for (const candidate of candidates) {
const amount = parseAmount(candidate)
if (amount != null) {
return formatAmountForRequest(amount)
}
}
return ''
}
export function validateAutoReloadInputs(
thresholdRaw: string,
reloadToRaw: string,
bounds: Pick<BillingStateResponse, 'max_usd' | 'min_usd'>,
copy: BillingCopy = DEFAULT_BILLING_COPY
): { error?: string; values?: { reloadTo: string; threshold: string } } {
const threshold = validateBillingAmount(copy.threshold, thresholdRaw, bounds, copy)
if (threshold.error || threshold.amount == null) {
return { error: threshold.error }
}
const reloadTo = validateBillingAmount(copy.reloadLabel, reloadToRaw, bounds, copy)
if (reloadTo.error || reloadTo.amount == null) {
return { error: reloadTo.error }
}
if (reloadTo.amount <= threshold.amount) {
return { error: copy.reloadGreater }
}
return {
values: {
reloadTo: formatAmountForRequest(reloadTo.amount),
threshold: formatAmountForRequest(threshold.amount)
}
}
}
export function validateBillingAmount(
label: string,
raw: string,
bounds: Pick<BillingStateResponse, 'max_usd' | 'min_usd'>,
copy: BillingCopy = DEFAULT_BILLING_COPY
): { amount?: number; error?: string } {
const cleaned = raw.trim().replace(/^\$/, '').trim()
if (!cleaned || !/^\d+(\.\d{1,2})?$/.test(cleaned)) {
return { error: copy.invalidAmount(label) }
}
const amount = Number(cleaned)
if (!(amount > 0)) {
return { error: copy.positiveAmount(label) }
}
const min = parseAmount(bounds.min_usd)
if (min != null && amount < min) {
return { error: copy.minAmount(label, formatMoney(min)) }
}
const max = parseAmount(bounds.max_usd)
if (max != null && amount > max) {
return { error: copy.maxAmount(label, formatMoney(max)) }
}
return { amount }
}
export function formatMoney(value?: null | number | string): string {
const amount = parseAmount(value)
if (amount == null) {
return EMPTY_BILLING_VALUE
}
return new Intl.NumberFormat(undefined, {
currency: 'USD',
maximumFractionDigits: amount % 1 === 0 ? 0 : 2,
minimumFractionDigits: amount % 1 === 0 ? 0 : 2,
style: 'currency'
}).format(amount)
}