diff --git a/packages/server/src/StripeManager.ts b/packages/server/src/StripeManager.ts index 0c8b41612..0c90cba6e 100644 --- a/packages/server/src/StripeManager.ts +++ b/packages/server/src/StripeManager.ts @@ -939,13 +939,21 @@ export class StripeManager { } as any, category: 'paid', name: `${selectedPackage.credits} Credits Purchase`, - metadata: { - usage_count: '0' - } + metadata: {} }) - // Clear cache - await this.cacheManager.del(`credits:balance:${customerId}`) + // Update Redis with new credit purchase + const existingCredits = await this.cacheManager.getCreditDataFromCache(customerId) + const newTotalCredits = (existingCredits?.totalCredits || 0) + selectedPackage.credits + const currentUsage = existingCredits?.totalUsage || 0 + const newAvailableCredits = newTotalCredits - currentUsage + + await this.cacheManager.updateCreditDataToCache(customerId, { + totalCredits: newTotalCredits, + totalUsage: currentUsage, + availableCredits: newAvailableCredits, + lastUpdated: Date.now() + }) return { invoice: paidInvoice, @@ -978,6 +986,7 @@ export class StripeManager { throw new Error('Subscription ID is required') } + // Get customer ID from subscription const subscription = await this.stripe.subscriptions.retrieve(subscriptionId) const customerId = subscription.customer as string @@ -985,6 +994,21 @@ export class StripeManager { throw new Error('Customer ID not found in subscription') } + // Try to get credit data from Redis first + const cachedCredits = await this.cacheManager.getCreditDataFromCache(customerId) + if (cachedCredits) { + return { + balance: cachedCredits.availableCredits * 100, // Convert to cents for backward compatibility + balanceInDollars: cachedCredits.availableCredits, + totalCredits: cachedCredits.totalCredits, + totalUsage: cachedCredits.totalUsage, + availableCredits: cachedCredits.availableCredits, + grants: [] // Simplified for Redis-based approach + } + } + + // Fallback to Stripe if no Redis data + const creditBalance = await this.stripe.billing.creditBalanceSummary.retrieve({ customer: customerId, filter: { @@ -1028,6 +1052,14 @@ export class StripeManager { const availableCredits = Math.max(0, totalCredits - totalUsage) + // Store in Redis for future requests + await this.cacheManager.updateCreditDataToCache(customerId, { + totalCredits, + totalUsage, + availableCredits, + lastUpdated: Date.now() + }) + return { balance, balanceInDollars, @@ -1221,80 +1253,20 @@ export class StripeManager { } try { - logger.info(`[updateCreditGrantUsage] Starting update for customer ${customerId}, quantity: ${quantity}`) + logger.info(`[updateCreditGrantUsage] Starting Redis update for customer ${customerId}, quantity: ${quantity}`) - // Get all credit grants for this customer (not just active ones) - const grants = await this.stripe.billing.creditGrants.list({ - customer: customerId, - limit: 100 - }) + // Update credit usage in Redis using customer ID + await this.cacheManager.incrementCreditUsage(customerId, quantity) - logger.info(`[updateCreditGrantUsage] Found ${grants.data.length} credit grants for customer ${customerId}`) - - if (grants.data.length === 0) { - logger.info('[updateCreditGrantUsage] No credit grants found for customer') - return - } - - // Sort by creation date (oldest first) to use FIFO approach - grants.data.sort((a, b) => a.created - b.created) - - // Find the grant that should be used for tracking usage - // We'll use the first grant that still has available credits or the oldest one - let grantToUpdate = grants.data.find((grant) => { - const effectiveBalance = (grant as any).effective_balance?.monetary?.value || 0 - logger.debug(`[updateCreditGrantUsage] Grant ${grant.id} has effective balance: ${effectiveBalance}`) - return effectiveBalance > 0 - }) - - // If no grant has remaining balance, use the most recent one for tracking - if (!grantToUpdate && grants.data.length > 0) { - grantToUpdate = grants.data[grants.data.length - 1] - logger.info(`[updateCreditGrantUsage] No grants with balance found, using most recent: ${grantToUpdate.id}`) - } - - if (!grantToUpdate) { - logger.warn('[updateCreditGrantUsage] No suitable grant found for usage tracking') - return - } - - // Validate metadata access - const metadata = grantToUpdate.metadata || {} - const currentUsageStr = metadata.usage_count || '0' - const currentUsage = parseInt(currentUsageStr) - - if (isNaN(currentUsage)) { - logger.error(`[updateCreditGrantUsage] Invalid usage_count in metadata: ${currentUsageStr}`) - return - } - - const newUsage = currentUsage + quantity - - logger.info(`[updateCreditGrantUsage] Updating grant ${grantToUpdate.id}: current usage ${currentUsage} -> ${newUsage}`) - - const updateResult = await this.stripe.billing.creditGrants.update(grantToUpdate.id, { - metadata: { - ...metadata, - usage_count: newUsage.toString() - } - }) - - logger.info(`[updateCreditGrantUsage] Successfully updated credit grant usage for grant ${grantToUpdate.id}`) - logger.debug(`[updateCreditGrantUsage] Updated metadata:`, updateResult.metadata) - - // Clear cache to ensure fresh data on next request - if (this.cacheManager) { - await this.cacheManager.del(`credits:balance:${customerId}`) - logger.debug(`[updateCreditGrantUsage] Cleared cache for customer ${customerId}`) - } + logger.info(`[updateCreditGrantUsage] Successfully updated credit usage in Redis for customer ${customerId}`) } catch (error) { - logger.error('[updateCreditGrantUsage] Error updating credit grant usage:', error) + logger.error('[updateCreditGrantUsage] Error updating credit usage in Redis:', error) // Log additional details for debugging if (error instanceof Error) { logger.error('[updateCreditGrantUsage] Error message:', error.message) logger.debug('[updateCreditGrantUsage] Error stack:', error.stack) } - // Don't throw here as meter usage was already reported successfully + throw error } } diff --git a/packages/server/src/UsageCacheManager.ts b/packages/server/src/UsageCacheManager.ts index 82fb451e1..48bb568be 100644 --- a/packages/server/src/UsageCacheManager.ts +++ b/packages/server/src/UsageCacheManager.ts @@ -162,6 +162,51 @@ export class UsageCacheManager { this.set(cacheKey, updatedData, 3600000) // Cache for 1 hour } + public async getCreditDataFromCache(customerId: string) { + const cacheKey = `credits:${customerId}` + return await this.get<{ + totalCredits: number + totalUsage: number + availableCredits: number + lastUpdated: number + }>(cacheKey) + } + + public async updateCreditDataToCache( + customerId: string, + creditData: { + totalCredits: number + totalUsage: number + availableCredits: number + lastUpdated: number + } + ) { + const cacheKey = `credits:${customerId}` + // No TTL for credit data to ensure persistence + this.set(cacheKey, creditData) + } + + public async incrementCreditUsage(customerId: string, quantity: number) { + if (!customerId || quantity <= 0) { + throw new Error('Invalid customer ID or quantity') + } + + const existingData = await this.getCreditDataFromCache(customerId) + if (!existingData) { + throw new Error(`No credit data found for customer ${customerId}. Please purchase credits first.`) + } + + const newUsage = existingData.totalUsage + quantity + const newAvailable = Math.max(0, existingData.totalCredits - newUsage) + + await this.updateCreditDataToCache(customerId, { + totalCredits: existingData.totalCredits, + totalUsage: newUsage, + availableCredits: newAvailable, + lastUpdated: Date.now() + }) + } + public async get(key: string): Promise { if (!this.cache) await this.initialize() const value = await this.cache.get(key) diff --git a/packages/ui/src/views/account/index.jsx b/packages/ui/src/views/account/index.jsx index 697b0fff9..6ce9c60f5 100644 --- a/packages/ui/src/views/account/index.jsx +++ b/packages/ui/src/views/account/index.jsx @@ -114,15 +114,15 @@ const AccountSettings = () => { const [creditsBalance, setCreditsBalance] = useState(null) const totalCredits = useMemo(() => { - return creditsBalance ? calculateTotalCredits(creditsBalance.grants) : 0 + return creditsBalance ? creditsBalance.totalCredits || 0 : 0 }, [creditsBalance]) const totalUsage = useMemo(() => { - return creditsBalance ? calculateTotalUsage(creditsBalance.grants) : 0 + return creditsBalance ? creditsBalance.totalUsage || 0 : 0 }, [creditsBalance]) const availableCredits = useMemo(() => { - return creditsBalance ? calculateAvailableCredits(creditsBalance.grants) : 0 + return creditsBalance ? creditsBalance.availableCredits || 0 : 0 }, [creditsBalance]) const [creditsPackages, setCreditsPackages] = useState([]) const [openCreditsDialog, setOpenCreditsDialog] = useState(false) @@ -514,12 +514,14 @@ const AccountSettings = () => { const response = await purchaseCreditsApi.request(packageType) - if (response.data?.success) { + // Check for success - either explicit success flag or 200 status code + if (response.data?.success || response.status === 200) { enqueueSnackbar({ message: 'Credits purchased successfully!', options: { key: new Date().getTime() + Math.random(), variant: 'success', + autoHideDuration: 10000, action: (key) => (