add shared comfy credit conversion helpers (#7061)

Introduces cents<->usd<->credit converters plus basic formatters and
adds test. Lays groundwork to start converting UI components into
displaying comfy credits.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7061-add-shared-comfy-credit-conversion-helpers-2bb6d73d3650810bb34fdf9bb3fc115b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Christian Byrne
2025-12-09 04:11:27 -08:00
committed by GitHub
parent 8209f5a108
commit aef40834f3
14 changed files with 628 additions and 159 deletions

View File

@@ -17,9 +17,9 @@ const mockSubscriptionData = {
}
const mockCreditsData = {
totalCredits: '10.00',
monthlyBonusCredits: '5.00',
prepaidCredits: '5.00',
totalCredits: '10.00 Credits',
monthlyBonusCredits: '5.00 Credits',
prepaidCredits: '5.00 Credits',
isLoadingBalance: false
}
@@ -154,8 +154,8 @@ describe('SubscriptionPanel', () => {
describe('credit display functionality', () => {
it('displays dynamic credit values correctly', () => {
const wrapper = createWrapper()
expect(wrapper.text()).toContain('$10.00') // totalCredits
expect(wrapper.text()).toContain('$5.00') // both monthlyBonus and prepaid
expect(wrapper.text()).toContain('10.00 Credits')
expect(wrapper.text()).toContain('5.00 Credits')
})
it('shows loading skeleton when fetching balance', () => {

View File

@@ -1,8 +1,27 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as comfyCredits from '@/base/credits/comfyCredits'
import { useSubscriptionCredits } from '@/platform/cloud/subscription/composables/useSubscriptionCredits'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import type { operations } from '@/types/comfyRegistryTypes'
type GetCustomerBalanceResponse =
operations['GetCustomerBalance']['responses']['200']['content']['application/json']
vi.mock(
'vue-i18n',
async (importOriginal: () => Promise<typeof import('vue-i18n')>) => {
const actual = await importOriginal()
return {
...actual,
useI18n: () => ({
t: () => 'Credits',
locale: { value: 'en-US' }
})
}
}
)
// Mock Firebase Auth and related modules
vi.mock('vuefire', () => ({
@@ -55,14 +74,6 @@ vi.mock('@/stores/apiKeyAuthStore', () => ({
})
}))
// Mock formatMetronomeCurrency
vi.mock('@/utils/formatUtil', () => ({
formatMetronomeCurrency: vi.fn((micros: number) => {
// Simple mock that converts micros to dollars
return (micros / 1000000).toFixed(2)
})
}))
describe('useSubscriptionCredits', () => {
let authStore: ReturnType<typeof useFirebaseAuthStore>
@@ -73,63 +84,66 @@ describe('useSubscriptionCredits', () => {
})
describe('totalCredits', () => {
it('should return "0.00" when balance is null', () => {
it('should return "0.00 Credits" when balance is null', () => {
authStore.balance = null
const { totalCredits } = useSubscriptionCredits()
expect(totalCredits.value).toBe('0.00')
expect(totalCredits.value).toBe('0.00 Credits')
})
it('should return "0.00" when amount_micros is missing', () => {
authStore.balance = {} as any
it('should return "0.00 Credits" when amount_micros is missing', () => {
authStore.balance = {} as GetCustomerBalanceResponse
const { totalCredits } = useSubscriptionCredits()
expect(totalCredits.value).toBe('0.00')
expect(totalCredits.value).toBe('0.00 Credits')
})
it('should format amount_micros correctly', () => {
authStore.balance = { amount_micros: 5000000 } as any
authStore.balance = { amount_micros: 100 } as GetCustomerBalanceResponse
const { totalCredits } = useSubscriptionCredits()
expect(totalCredits.value).toBe('5.00')
expect(totalCredits.value).toBe('211.00 Credits')
})
it('should handle formatting errors gracefully', async () => {
const mockFormatMetronomeCurrency = vi.mocked(
await import('@/utils/formatUtil')
).formatMetronomeCurrency
mockFormatMetronomeCurrency.mockImplementationOnce(() => {
it('should handle formatting errors by throwing', async () => {
const formatSpy = vi.spyOn(comfyCredits, 'formatCreditsFromCents')
formatSpy.mockImplementationOnce(() => {
throw new Error('Formatting error')
})
authStore.balance = { amount_micros: 5000000 } as any
authStore.balance = { amount_micros: 100 } as GetCustomerBalanceResponse
const { totalCredits } = useSubscriptionCredits()
expect(totalCredits.value).toBe('0.00')
expect(() => totalCredits.value).toThrow('Formatting error')
formatSpy.mockRestore()
})
})
describe('monthlyBonusCredits', () => {
it('should return "0.00" when cloud_credit_balance_micros is missing', () => {
authStore.balance = {} as any
it('should return "0.00 Credits" when cloud_credit_balance_micros is missing', () => {
authStore.balance = {} as GetCustomerBalanceResponse
const { monthlyBonusCredits } = useSubscriptionCredits()
expect(monthlyBonusCredits.value).toBe('0.00')
expect(monthlyBonusCredits.value).toBe('0.00 Credits')
})
it('should format cloud_credit_balance_micros correctly', () => {
authStore.balance = { cloud_credit_balance_micros: 2500000 } as any
authStore.balance = {
cloud_credit_balance_micros: 200
} as GetCustomerBalanceResponse
const { monthlyBonusCredits } = useSubscriptionCredits()
expect(monthlyBonusCredits.value).toBe('2.50')
expect(monthlyBonusCredits.value).toBe('422.00 Credits')
})
})
describe('prepaidCredits', () => {
it('should return "0.00" when prepaid_balance_micros is missing', () => {
authStore.balance = {} as any
it('should return "0.00 Credits" when prepaid_balance_micros is missing', () => {
authStore.balance = {} as GetCustomerBalanceResponse
const { prepaidCredits } = useSubscriptionCredits()
expect(prepaidCredits.value).toBe('0.00')
expect(prepaidCredits.value).toBe('0.00 Credits')
})
it('should format prepaid_balance_micros correctly', () => {
authStore.balance = { prepaid_balance_micros: 7500000 } as any
authStore.balance = {
prepaid_balance_micros: 300
} as GetCustomerBalanceResponse
const { prepaidCredits } = useSubscriptionCredits()
expect(prepaidCredits.value).toBe('7.50')
expect(prepaidCredits.value).toBe('633.00 Credits')
})
})