Skip to content

Commit c21ded6

Browse files
feat: add comfy credit domain helpers
1 parent 29dbfa3 commit c21ded6

File tree

2 files changed

+252
-0
lines changed

2 files changed

+252
-0
lines changed

src/base/credits/comfyCredits.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/**
2+
* Fixed conversion rate between USD and Comfy credits.
3+
* 1 credit costs 210 cents ($2.10).
4+
*/
5+
export const COMFY_CREDIT_RATE_CENTS = 210
6+
7+
export const COMFY_CREDIT_RATE_USD = COMFY_CREDIT_RATE_CENTS / 100
8+
9+
const DEFAULT_NUMBER_FORMAT: Intl.NumberFormatOptions = {
10+
minimumFractionDigits: 2,
11+
maximumFractionDigits: 2
12+
}
13+
14+
const formatNumber = (
15+
value: number,
16+
options: Intl.NumberFormatOptions = DEFAULT_NUMBER_FORMAT,
17+
locale?: string
18+
) => {
19+
const merged: Intl.NumberFormatOptions = {
20+
...DEFAULT_NUMBER_FORMAT,
21+
...options
22+
}
23+
24+
if (
25+
typeof merged.maximumFractionDigits === 'number' &&
26+
typeof merged.minimumFractionDigits === 'number' &&
27+
merged.maximumFractionDigits < merged.minimumFractionDigits
28+
) {
29+
merged.minimumFractionDigits = merged.maximumFractionDigits
30+
}
31+
32+
return new Intl.NumberFormat(locale, merged).format(value)
33+
}
34+
35+
export const centsToUsd = (cents: number): number => cents / 100
36+
export const usdToCents = (usd: number): number => Math.round(usd * 100)
37+
38+
/**
39+
* Converts a USD amount into Comfy credits.
40+
*/
41+
export function usdToComfyCredits(usd: number): number {
42+
return usd / COMFY_CREDIT_RATE_USD
43+
}
44+
45+
/**
46+
* Converts USD cents into Comfy credits.
47+
*/
48+
export function centsToComfyCredits(cents: number): number {
49+
return cents / COMFY_CREDIT_RATE_CENTS
50+
}
51+
52+
/**
53+
* Converts Comfy credits back to USD.
54+
*/
55+
export function comfyCreditsToUsd(credits: number): number {
56+
return credits * COMFY_CREDIT_RATE_USD
57+
}
58+
59+
/**
60+
* Converts Comfy credits to cents.
61+
*/
62+
export function comfyCreditsToCents(credits: number): number {
63+
return credits * COMFY_CREDIT_RATE_CENTS
64+
}
65+
66+
export function formatUsdFromCents(
67+
cents: number,
68+
options?: Intl.NumberFormatOptions,
69+
locale?: string
70+
): string {
71+
return formatNumber(
72+
centsToUsd(cents),
73+
{ ...DEFAULT_NUMBER_FORMAT, ...options },
74+
locale
75+
)
76+
}
77+
78+
/**
79+
* Formats credits to a localized numeric string (no unit suffix).
80+
*/
81+
export function formatComfyCreditsAmount(
82+
credits: number,
83+
options?: Intl.NumberFormatOptions,
84+
locale?: string
85+
): string {
86+
return formatNumber(credits, { ...DEFAULT_NUMBER_FORMAT, ...options }, locale)
87+
}
88+
89+
type FormatCreditsOptions = {
90+
unit?: string | null
91+
numberOptions?: Intl.NumberFormatOptions
92+
locale?: string
93+
}
94+
95+
export function formatComfyCreditsLabel(
96+
credits: number,
97+
{ unit = 'credits', numberOptions, locale }: FormatCreditsOptions = {}
98+
): string {
99+
const formatted = formatComfyCreditsAmount(credits, numberOptions, locale)
100+
return unit ? `${formatted} ${unit}` : formatted
101+
}
102+
103+
export function formatComfyCreditsLabelFromCents(
104+
cents: number,
105+
options?: FormatCreditsOptions
106+
): string {
107+
return formatComfyCreditsLabel(centsToComfyCredits(cents), options)
108+
}
109+
110+
export function formatComfyCreditsLabelFromUsd(
111+
usd: number,
112+
options?: FormatCreditsOptions
113+
): string {
114+
return formatComfyCreditsLabel(usdToComfyCredits(usd), options)
115+
}
116+
117+
export function formatComfyCreditsRangeLabelFromUsd(
118+
minUsd: number,
119+
maxUsd: number,
120+
{
121+
unit = 'credits',
122+
numberOptions,
123+
locale,
124+
separator = '–'
125+
}: FormatCreditsOptions & {
126+
separator?: string
127+
} = {}
128+
): string {
129+
const min = formatComfyCreditsAmount(
130+
usdToComfyCredits(minUsd),
131+
numberOptions,
132+
locale
133+
)
134+
const max = formatComfyCreditsAmount(
135+
usdToComfyCredits(maxUsd),
136+
numberOptions,
137+
locale
138+
)
139+
const joined = `${min}${separator}${max}`
140+
return unit ? `${joined} ${unit}` : joined
141+
}
142+
143+
const USD_RANGE_REGEX = /(~?)\$(\d+(?:\.\d+)?)\s*[-]\s*\$?(\d+(?:\.\d+)?)/g
144+
const USD_VALUE_REGEX = /(~?)\$(\d+(?:\.\d+)?)/g
145+
146+
/**
147+
* Converts a USD-denoted string (e.g., "$0.45-1.2/Run") into a credits string.
148+
* Any "$X" occurrences become "Y credits". Ranges are rendered as "Y–Z credits".
149+
*/
150+
export function convertUsdLabelToCredits(
151+
label: string,
152+
options?: FormatCreditsOptions
153+
): string {
154+
if (!label) return label
155+
const unit = options?.unit ?? 'credits'
156+
const numberOptions = options?.numberOptions
157+
const locale = options?.locale
158+
159+
const formatSingle = (usd: number) =>
160+
formatComfyCreditsLabel(usdToComfyCredits(usd), {
161+
unit,
162+
numberOptions,
163+
locale
164+
})
165+
166+
const formatRange = (min: number, max: number, prefix = '') => {
167+
const minStr = formatComfyCreditsAmount(
168+
usdToComfyCredits(min),
169+
numberOptions,
170+
locale
171+
)
172+
const maxStr = formatComfyCreditsAmount(
173+
usdToComfyCredits(max),
174+
numberOptions,
175+
locale
176+
)
177+
const joined = `${minStr}${maxStr}`
178+
return unit ? `${prefix}${joined} ${unit}` : `${prefix}${joined}`
179+
}
180+
181+
let converted = label
182+
converted = converted.replace(
183+
USD_RANGE_REGEX,
184+
(_match, prefix = '', minUsd, maxUsd) =>
185+
formatRange(parseFloat(minUsd), parseFloat(maxUsd), prefix)
186+
)
187+
188+
converted = converted.replace(
189+
USD_VALUE_REGEX,
190+
(_match, prefix = '', amount) => {
191+
const formatted = formatSingle(parseFloat(amount))
192+
return `${prefix}${formatted}`
193+
}
194+
)
195+
196+
return converted
197+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import {
4+
COMFY_CREDIT_RATE_CENTS,
5+
COMFY_CREDIT_RATE_USD,
6+
centsToComfyCredits,
7+
comfyCreditsToCents,
8+
comfyCreditsToUsd,
9+
convertUsdLabelToCredits,
10+
formatComfyCreditsAmount,
11+
formatComfyCreditsLabel,
12+
formatComfyCreditsRangeLabelFromUsd,
13+
usdToComfyCredits
14+
} from '@/base/credits/comfyCredits'
15+
16+
describe('comfyCredits helpers', () => {
17+
it('exposes the fixed conversion rate', () => {
18+
expect(COMFY_CREDIT_RATE_CENTS).toBe(210)
19+
expect(COMFY_CREDIT_RATE_USD).toBeCloseTo(2.1)
20+
})
21+
22+
it('converts USD and cents to credits', () => {
23+
expect(usdToComfyCredits(2.1)).toBeCloseTo(1)
24+
expect(usdToComfyCredits(10.5)).toBeCloseTo(5)
25+
expect(centsToComfyCredits(210)).toBeCloseTo(1)
26+
expect(centsToComfyCredits(1050)).toBeCloseTo(5)
27+
})
28+
29+
it('converts credits back to USD and cents', () => {
30+
expect(comfyCreditsToUsd(1)).toBeCloseTo(2.1)
31+
expect(comfyCreditsToUsd(3.5)).toBeCloseTo(7.35)
32+
expect(comfyCreditsToCents(1)).toBe(210)
33+
expect(comfyCreditsToCents(3.5)).toBeCloseTo(735)
34+
})
35+
36+
it('formats credits with localized precision', () => {
37+
expect(formatComfyCreditsAmount(1234.5678)).toBe('1,234.57')
38+
expect(
39+
formatComfyCreditsLabel(1.2345, {
40+
unit: 'credits',
41+
numberOptions: { maximumFractionDigits: 1 }
42+
})
43+
).toBe('1.2 credits')
44+
})
45+
46+
it('formats ranges and USD strings into credits', () => {
47+
expect(formatComfyCreditsRangeLabelFromUsd(2.1, 4.2)).toBe(
48+
'1.00–2.00 credits'
49+
)
50+
expect(convertUsdLabelToCredits('$2.10/Run')).toBe('1.00 credits/Run')
51+
expect(convertUsdLabelToCredits('~$2.10-$4.20/Run')).toBe(
52+
'~1.00–2.00 credits/Run'
53+
)
54+
})
55+
})

0 commit comments

Comments
 (0)