Store credits purchased and used in Redis instead of reading/writing to Stripe
This commit is contained in:
parent
e612716c7c
commit
ddfecf725c
|
|
@ -939,13 +939,21 @@ export class StripeManager {
|
||||||
} as any,
|
} as any,
|
||||||
category: 'paid',
|
category: 'paid',
|
||||||
name: `${selectedPackage.credits} Credits Purchase`,
|
name: `${selectedPackage.credits} Credits Purchase`,
|
||||||
metadata: {
|
metadata: {}
|
||||||
usage_count: '0'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Clear cache
|
// Update Redis with new credit purchase
|
||||||
await this.cacheManager.del(`credits:balance:${customerId}`)
|
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 {
|
return {
|
||||||
invoice: paidInvoice,
|
invoice: paidInvoice,
|
||||||
|
|
@ -978,6 +986,7 @@ export class StripeManager {
|
||||||
throw new Error('Subscription ID is required')
|
throw new Error('Subscription ID is required')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get customer ID from subscription
|
||||||
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId)
|
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId)
|
||||||
const customerId = subscription.customer as string
|
const customerId = subscription.customer as string
|
||||||
|
|
||||||
|
|
@ -985,6 +994,21 @@ export class StripeManager {
|
||||||
throw new Error('Customer ID not found in subscription')
|
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({
|
const creditBalance = await this.stripe.billing.creditBalanceSummary.retrieve({
|
||||||
customer: customerId,
|
customer: customerId,
|
||||||
filter: {
|
filter: {
|
||||||
|
|
@ -1028,6 +1052,14 @@ export class StripeManager {
|
||||||
|
|
||||||
const availableCredits = Math.max(0, totalCredits - totalUsage)
|
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 {
|
return {
|
||||||
balance,
|
balance,
|
||||||
balanceInDollars,
|
balanceInDollars,
|
||||||
|
|
@ -1221,80 +1253,20 @@ export class StripeManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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)
|
// Update credit usage in Redis using customer ID
|
||||||
const grants = await this.stripe.billing.creditGrants.list({
|
await this.cacheManager.incrementCreditUsage(customerId, quantity)
|
||||||
customer: customerId,
|
|
||||||
limit: 100
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info(`[updateCreditGrantUsage] Found ${grants.data.length} credit grants for customer ${customerId}`)
|
logger.info(`[updateCreditGrantUsage] Successfully updated credit usage in Redis 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}`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} 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
|
// Log additional details for debugging
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
logger.error('[updateCreditGrantUsage] Error message:', error.message)
|
logger.error('[updateCreditGrantUsage] Error message:', error.message)
|
||||||
logger.debug('[updateCreditGrantUsage] Error stack:', error.stack)
|
logger.debug('[updateCreditGrantUsage] Error stack:', error.stack)
|
||||||
}
|
}
|
||||||
// Don't throw here as meter usage was already reported successfully
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,51 @@ export class UsageCacheManager {
|
||||||
this.set(cacheKey, updatedData, 3600000) // Cache for 1 hour
|
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<T>(key: string): Promise<T | null> {
|
public async get<T>(key: string): Promise<T | null> {
|
||||||
if (!this.cache) await this.initialize()
|
if (!this.cache) await this.initialize()
|
||||||
const value = await this.cache.get<T>(key)
|
const value = await this.cache.get<T>(key)
|
||||||
|
|
|
||||||
|
|
@ -114,15 +114,15 @@ const AccountSettings = () => {
|
||||||
const [creditsBalance, setCreditsBalance] = useState(null)
|
const [creditsBalance, setCreditsBalance] = useState(null)
|
||||||
|
|
||||||
const totalCredits = useMemo(() => {
|
const totalCredits = useMemo(() => {
|
||||||
return creditsBalance ? calculateTotalCredits(creditsBalance.grants) : 0
|
return creditsBalance ? creditsBalance.totalCredits || 0 : 0
|
||||||
}, [creditsBalance])
|
}, [creditsBalance])
|
||||||
|
|
||||||
const totalUsage = useMemo(() => {
|
const totalUsage = useMemo(() => {
|
||||||
return creditsBalance ? calculateTotalUsage(creditsBalance.grants) : 0
|
return creditsBalance ? creditsBalance.totalUsage || 0 : 0
|
||||||
}, [creditsBalance])
|
}, [creditsBalance])
|
||||||
|
|
||||||
const availableCredits = useMemo(() => {
|
const availableCredits = useMemo(() => {
|
||||||
return creditsBalance ? calculateAvailableCredits(creditsBalance.grants) : 0
|
return creditsBalance ? creditsBalance.availableCredits || 0 : 0
|
||||||
}, [creditsBalance])
|
}, [creditsBalance])
|
||||||
const [creditsPackages, setCreditsPackages] = useState([])
|
const [creditsPackages, setCreditsPackages] = useState([])
|
||||||
const [openCreditsDialog, setOpenCreditsDialog] = useState(false)
|
const [openCreditsDialog, setOpenCreditsDialog] = useState(false)
|
||||||
|
|
@ -514,12 +514,14 @@ const AccountSettings = () => {
|
||||||
|
|
||||||
const response = await purchaseCreditsApi.request(packageType)
|
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({
|
enqueueSnackbar({
|
||||||
message: 'Credits purchased successfully!',
|
message: 'Credits purchased successfully!',
|
||||||
options: {
|
options: {
|
||||||
key: new Date().getTime() + Math.random(),
|
key: new Date().getTime() + Math.random(),
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
|
autoHideDuration: 10000,
|
||||||
action: (key) => (
|
action: (key) => (
|
||||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||||
<IconX />
|
<IconX />
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue