-
Notifications
You must be signed in to change notification settings - Fork 433
add shared comfy credit conversion helpers #7061
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| const DEFAULT_NUMBER_FORMAT: Intl.NumberFormatOptions = { | ||
| minimumFractionDigits: 2, | ||
| maximumFractionDigits: 2 | ||
| } | ||
|
|
||
| const formatNumber = ({ | ||
| value, | ||
| locale, | ||
| options | ||
| }: { | ||
| value: number | ||
| locale?: string | ||
| options?: Intl.NumberFormatOptions | ||
| }): string => { | ||
| const merged: Intl.NumberFormatOptions = { | ||
| ...DEFAULT_NUMBER_FORMAT, | ||
| ...options | ||
| } | ||
|
|
||
| if ( | ||
| typeof merged.maximumFractionDigits === 'number' && | ||
| typeof merged.minimumFractionDigits === 'number' && | ||
| merged.maximumFractionDigits < merged.minimumFractionDigits | ||
| ) { | ||
| merged.minimumFractionDigits = merged.maximumFractionDigits | ||
| } | ||
|
|
||
| return new Intl.NumberFormat(locale, merged).format(value) | ||
| } | ||
|
|
||
| export const COMFY_CREDIT_RATE_CENTS = 210 | ||
| export const COMFY_CREDIT_RATE_USD = COMFY_CREDIT_RATE_CENTS / 100 | ||
|
|
||
| export const usdToCents = (usd: number): number => Math.round(usd * 100) | ||
|
|
||
| export const centsToCredits = (cents: number): number => | ||
| cents / COMFY_CREDIT_RATE_CENTS | ||
|
|
||
| export const creditsToCents = (credits: number): number => | ||
| Math.round(credits * COMFY_CREDIT_RATE_CENTS) | ||
|
|
||
| export const usdToCredits = (usd: number): number => | ||
| centsToCredits(usdToCents(usd)) | ||
|
|
||
| export const creditsToUsd = (credits: number): number => | ||
| creditsToCents(credits) / 100 | ||
|
|
||
| export type FormatOptions = { | ||
| value: number | ||
| locale?: string | ||
| numberOptions?: Intl.NumberFormatOptions | ||
| } | ||
|
|
||
| export const formatCredits = ({ | ||
| value, | ||
| locale, | ||
| numberOptions | ||
| }: FormatOptions): string => | ||
| formatNumber({ value, locale, options: numberOptions }) | ||
|
|
||
| export const formatCreditsFromCents = ({ | ||
| cents, | ||
| locale, | ||
| numberOptions | ||
| }: { | ||
| cents: number | ||
| locale?: string | ||
| numberOptions?: Intl.NumberFormatOptions | ||
| }): string => | ||
| formatCredits({ | ||
| value: centsToCredits(cents), | ||
| locale, | ||
| numberOptions | ||
| }) | ||
|
|
||
| export const formatCreditsFromUsd = ({ | ||
| usd, | ||
| locale, | ||
| numberOptions | ||
| }: { | ||
| usd: number | ||
| locale?: string | ||
| numberOptions?: Intl.NumberFormatOptions | ||
| }): string => | ||
| formatCredits({ | ||
| value: usdToCredits(usd), | ||
| locale, | ||
| numberOptions | ||
| }) | ||
|
|
||
| export const formatUsd = ({ | ||
| value, | ||
| locale, | ||
| numberOptions | ||
| }: FormatOptions): string => | ||
| formatNumber({ | ||
| value, | ||
| locale, | ||
| options: numberOptions | ||
| }) | ||
|
|
||
| export const formatUsdFromCents = ({ | ||
| cents, | ||
| locale, | ||
| numberOptions | ||
| }: { | ||
| cents: number | ||
| locale?: string | ||
| numberOptions?: Intl.NumberFormatOptions | ||
| }): string => | ||
| formatUsd({ | ||
| value: cents / 100, | ||
| locale, | ||
| numberOptions | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,56 +1,59 @@ | ||
| import { computed } from 'vue' | ||
| import { useI18n } from 'vue-i18n' | ||
|
|
||
| import { formatCreditsFromCents } from '@/base/credits/comfyCredits' | ||
| import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' | ||
| import { formatMetronomeCurrency } from '@/utils/formatUtil' | ||
|
|
||
| /** | ||
| * Composable for handling subscription credit calculations and formatting | ||
| */ | ||
| export function useSubscriptionCredits() { | ||
| const authStore = useFirebaseAuthStore() | ||
| const { t, locale } = useI18n() | ||
|
|
||
| const formatBalance = (maybeCents?: number) => { | ||
| // Backend returns cents despite the *_micros naming convention. | ||
| const cents = maybeCents ?? 0 | ||
| const amount = formatCreditsFromCents({ | ||
| cents, | ||
| locale: locale.value | ||
| }) | ||
| return `${amount} ${t('credits.credits')}` | ||
| } | ||
|
Comment on lines
+14
to
+22
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Search for backend API response types related to subscription/credits
rg -n "GetCustomerBalance|cloud_credit_balance|prepaid_balance" --type ts -C 5Repository: Comfy-Org/ComfyUI_frontend Length of output: 16028 🏁 Script executed: # Look for type definitions and API responses
fd -e ts -e tsx | xargs rg -l "micros" | head -20Repository: Comfy-Org/ComfyUI_frontend Length of output: 390 🏁 Script executed: # Search for any conversion or formatting logic around these fields
rg -n "micros|formatCredits" --type ts --type tsx -A 3 -B 3Repository: Comfy-Org/ComfyUI_frontend Length of output: 97 🏁 Script executed: # Check for API schema or backend type definitions
fd -e ts -e tsx | xargs rg -l "backend.*return|api.*response" | head -10Repository: Comfy-Org/ComfyUI_frontend Length of output: 164 🏁 Script executed: # Find formatCreditsFromCents implementation
rg -n "formatCreditsFromCents" --type ts -B 5 -A 15Repository: Comfy-Org/ComfyUI_frontend Length of output: 9756 🏁 Script executed: # Look for any constants or documentation about the 210 division factor
rg -n "210|micros|cents" --type ts -C 3 | head -100Repository: Comfy-Org/ComfyUI_frontend Length of output: 6997 🏁 Script executed: # Check the exact API documentation comment about microamount
rg -n "microamount|micros" packages/registry-types/src/comfyRegistryTypes.ts -B 2 -A 2 | head -60Repository: Comfy-Org/ComfyUI_frontend Length of output: 3006 Backend API contract violation: field names and documentation don't match unit values. The backend API schema documents
The comment on line 15 is misleading—it's not a naming issue; the fields ARE correctly named with This needs clarification from the backend team or verification through actual API testing to ensure the unit conversion is correct. 🤖 Prompt for AI Agents |
||
|
|
||
| const totalCredits = computed(() => { | ||
| if (!authStore.balance?.amount_micros) return '0.00' | ||
| try { | ||
| return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd') | ||
| return formatBalance(authStore.balance?.amount_micros) | ||
| } catch (error) { | ||
| console.error( | ||
| '[useSubscriptionCredits] Error formatting total credits:', | ||
| error | ||
| ) | ||
| return '0.00' | ||
| return formatBalance(0) | ||
| } | ||
| }) | ||
|
|
||
| const monthlyBonusCredits = computed(() => { | ||
| if (!authStore.balance?.cloud_credit_balance_micros) return '0.00' | ||
| try { | ||
| return formatMetronomeCurrency( | ||
| authStore.balance.cloud_credit_balance_micros, | ||
| 'usd' | ||
| ) | ||
| return formatBalance(authStore.balance?.cloud_credit_balance_micros) | ||
| } catch (error) { | ||
| console.error( | ||
| '[useSubscriptionCredits] Error formatting monthly bonus credits:', | ||
| error | ||
| ) | ||
| return '0.00' | ||
| return formatBalance(0) | ||
| } | ||
| }) | ||
|
|
||
| const prepaidCredits = computed(() => { | ||
| if (!authStore.balance?.prepaid_balance_micros) return '0.00' | ||
| try { | ||
| return formatMetronomeCurrency( | ||
| authStore.balance.prepaid_balance_micros, | ||
| 'usd' | ||
| ) | ||
| return formatBalance(authStore.balance?.prepaid_balance_micros) | ||
| } catch (error) { | ||
| console.error( | ||
| '[useSubscriptionCredits] Error formatting prepaid credits:', | ||
| error | ||
| ) | ||
| return '0.00' | ||
| return formatBalance(0) | ||
| } | ||
| }) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { describe, expect, test } from 'vitest' | ||
|
|
||
| import { | ||
| COMFY_CREDIT_RATE_CENTS, | ||
| COMFY_CREDIT_RATE_USD, | ||
| centsToCredits, | ||
| creditsToCents, | ||
| creditsToUsd, | ||
| formatCredits, | ||
| formatCreditsFromCents, | ||
| formatCreditsFromUsd, | ||
| formatUsd, | ||
| formatUsdFromCents, | ||
| usdToCents, | ||
| usdToCredits | ||
| } from '@/base/credits/comfyCredits' | ||
|
|
||
| describe('comfyCredits helpers', () => { | ||
| test('exposes the fixed conversion rate', () => { | ||
| expect(COMFY_CREDIT_RATE_CENTS).toBe(210) | ||
| expect(COMFY_CREDIT_RATE_USD).toBeCloseTo(2.1) | ||
| }) | ||
|
|
||
| test('converts between USD and cents', () => { | ||
| expect(usdToCents(1.23)).toBe(123) | ||
| expect(formatUsdFromCents({ cents: 123, locale: 'en-US' })).toBe('1.23') | ||
| }) | ||
|
|
||
| test('converts cents to credits and back', () => { | ||
| expect(centsToCredits(210)).toBeCloseTo(1) | ||
| expect(creditsToCents(5)).toBe(1050) | ||
| }) | ||
|
|
||
| test('converts USD to credits and back', () => { | ||
| expect(usdToCredits(2.1)).toBeCloseTo(1) | ||
| expect(creditsToUsd(3.5)).toBeCloseTo(7.35) | ||
| }) | ||
|
|
||
| test('formats credits and USD values using en-US locale', () => { | ||
| const locale = 'en-US' | ||
| expect(formatCredits({ value: 1234.567, locale })).toBe('1,234.57') | ||
| expect(formatCreditsFromCents({ cents: 210, locale })).toBe('1.00') | ||
| expect(formatCreditsFromUsd({ usd: 4.2, locale })).toBe('2.00') | ||
| expect(formatUsd({ value: 4.2, locale })).toBe('4.20') | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider simplifying function signatures with parameter reordering.
Past review comments suggest that having
undefinedas a placeholder for locale (e.g.,formatCredits(value, undefined, options)) is awkward. The current object-based parameters avoid this, but another approach would be to matchIntl.NumberFormat's parameter order:(value, locale?, options?).Example alternative signature:
This would allow calls like:
However, the current object-based approach is also valid and provides better clarity at call sites, especially when only some parameters are needed. Consider team preferences and consistency with other utility functions in the codebase.
Based on past review discussion about parameter ergonomics.