Fix issues when purchasing credits

This commit is contained in:
Ilango 2025-07-21 14:32:08 +05:30
parent 051d8ef401
commit dbf5a2edc5
3 changed files with 70 additions and 34 deletions

View File

@ -5,6 +5,7 @@ import { UserPlan } from './Interface'
import { GeneralErrorMessage, LICENSE_QUOTAS } from './utils/constants' import { GeneralErrorMessage, LICENSE_QUOTAS } from './utils/constants'
import { InternalFlowiseError } from './errors/internalFlowiseError' import { InternalFlowiseError } from './errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes' import { StatusCodes } from 'http-status-codes'
import logger from './utils/logger'
export class StripeManager { export class StripeManager {
private static instance: StripeManager private static instance: StripeManager
@ -826,7 +827,10 @@ export class StripeManager {
const creditBalance = await this.stripe!.billing.creditBalanceSummary.retrieve({ const creditBalance = await this.stripe!.billing.creditBalanceSummary.retrieve({
customer: customerId, customer: customerId,
filter: { filter: {
type: 'monetary' as any type: 'applicability_scope',
applicability_scope: {
price_type: 'metered'
}
} }
}) })
@ -849,6 +853,10 @@ export class StripeManager {
} }
public async purchaseCredits(subscriptionId: string, packageType: string): Promise<Record<string, any>> { public async purchaseCredits(subscriptionId: string, packageType: string): Promise<Record<string, any>> {
if (!this.stripe) {
throw new Error('Stripe is not initialized')
}
try { try {
if (!process.env.CREDIT_PRODUCT_ID) { if (!process.env.CREDIT_PRODUCT_ID) {
throw new Error('CREDIT_PRODUCT_ID environment variable is required') throw new Error('CREDIT_PRODUCT_ID environment variable is required')
@ -860,7 +868,7 @@ export class StripeManager {
throw new Error('Subscription ID and package type are required') throw new Error('Subscription ID and package type are required')
} }
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
if (!customerId) { if (!customerId) {
@ -869,28 +877,40 @@ export class StripeManager {
// Get credit packages // Get credit packages
const packages = await this.getCreditsPackages() const packages = await this.getCreditsPackages()
logger.info(`Retrieved credit packages: ${JSON.stringify(packages)}`)
const selectedPackage = packages.find((pkg: any) => pkg.id === packageType) const selectedPackage = packages.find((pkg: any) => pkg.id === packageType)
logger.info(`Selected credit packages: ${JSON.stringify(selectedPackage)}`)
if (!selectedPackage) { if (!selectedPackage) {
throw new Error(`No active price found for ${packageType} package`) throw new Error(`No active price found for ${packageType} package`)
} }
// Create invoice for credit purchase // Create invoice for credit purchase
const invoiceItem = await this.stripe!.invoiceItems.create({ const invoice = await this.stripe.invoices.create({
customer: customerId,
amount: selectedPackage.price,
currency: 'usd',
description: `${selectedPackage.credits} Credits Package`
})
const invoice = await this.stripe!.invoices.create({
customer: customerId, customer: customerId,
auto_advance: true, auto_advance: true,
collection_method: 'charge_automatically' collection_method: 'charge_automatically'
}) })
const finalizedInvoice = await this.stripe!.invoices.finalizeInvoice(invoice.id!) if (!invoice.id) {
const paidInvoice = await this.stripe!.invoices.pay(finalizedInvoice.id!) throw new Error('Invoice creation failed')
}
await this.stripe.invoiceItems.create({
customer: customerId,
amount: selectedPackage.price,
invoice: invoice.id,
currency: 'usd',
description: `${selectedPackage.credits} Credits Package`
})
const finalizedInvoice = await this.stripe.invoices.finalizeInvoice(invoice.id)
if (!finalizedInvoice.id) {
throw new Error('Failed to finalize invoice')
}
const paidInvoice = await this.stripe.invoices.pay(finalizedInvoice.id)
if (paidInvoice.status !== 'paid') { if (paidInvoice.status !== 'paid') {
throw new Error('Payment failed') throw new Error('Payment failed')
@ -899,8 +919,11 @@ export class StripeManager {
// Add metered subscription item if it doesn't exist // Add metered subscription item if it doesn't exist
const meteredItemResult = await this.addMeteredSubscriptionItem(subscriptionId) const meteredItemResult = await this.addMeteredSubscriptionItem(subscriptionId)
// Get price
const price = await this.stripe.prices.retrieve(process.env.METERED_PRICE_ID!)
// Create credit grant // Create credit grant
const creditGrant = await this.stripe!.billing.creditGrants.create({ const creditGrant = await this.stripe.billing.creditGrants.create({
customer: customerId, customer: customerId,
amount: { amount: {
type: 'monetary' as any, type: 'monetary' as any,
@ -911,8 +934,7 @@ export class StripeManager {
}, },
applicability_config: { applicability_config: {
scope: { scope: {
price_type: 'metered', prices: [{ id: price.id }]
prices: [process.env.METERED_PRICE_ID!]
} }
} as any, } as any,
category: 'paid', category: 'paid',
@ -951,7 +973,10 @@ export class StripeManager {
const creditBalance = await this.stripe!.billing.creditBalanceSummary.retrieve({ const creditBalance = await this.stripe!.billing.creditBalanceSummary.retrieve({
customer: customerId, customer: customerId,
filter: { filter: {
type: 'monetary' as any type: 'applicability_scope',
applicability_scope: {
price_type: 'metered'
}
} }
}) })
@ -1063,6 +1088,10 @@ export class StripeManager {
} }
public async addMeteredSubscriptionItem(subscriptionId: string): Promise<{ added: boolean; message: string }> { public async addMeteredSubscriptionItem(subscriptionId: string): Promise<{ added: boolean; message: string }> {
if (!this.stripe) {
throw new Error('Stripe is not initialized')
}
try { try {
if (!process.env.METERED_PRICE_ID) { if (!process.env.METERED_PRICE_ID) {
throw new Error('METERED_PRICE_ID environment variable is required') throw new Error('METERED_PRICE_ID environment variable is required')
@ -1071,7 +1100,7 @@ export class StripeManager {
throw new Error('Subscription ID is required') throw new Error('Subscription ID is required')
} }
const subscription = await this.stripe!.subscriptions.retrieve(subscriptionId) const subscription = await this.stripe.subscriptions.retrieve(subscriptionId)
// Check if metered item already exists // Check if metered item already exists
const existingMeteredItem = subscription.items.data.find((item) => item.price.id === process.env.METERED_PRICE_ID) const existingMeteredItem = subscription.items.data.find((item) => item.price.id === process.env.METERED_PRICE_ID)
@ -1084,10 +1113,9 @@ export class StripeManager {
} }
// Add metered subscription item // Add metered subscription item
await this.stripe!.subscriptionItems.create({ await this.stripe.subscriptionItems.create({
subscription: subscriptionId, subscription: subscriptionId,
price: process.env.METERED_PRICE_ID!, price: process.env.METERED_PRICE_ID!
quantity: 0
}) })
return { return {

View File

@ -5,21 +5,21 @@ import { MODE } from './Interface'
import { LICENSE_QUOTAS } from './utils/constants' import { LICENSE_QUOTAS } from './utils/constants'
import { StripeManager } from './StripeManager' import { StripeManager } from './StripeManager'
const DISABLED_QUOTAS = { const getDisabledQuotas = () => ({
[LICENSE_QUOTAS.PREDICTIONS_LIMIT]: 0, [LICENSE_QUOTAS.PREDICTIONS_LIMIT]: 0,
[LICENSE_QUOTAS.STORAGE_LIMIT]: 0, // in MB [LICENSE_QUOTAS.STORAGE_LIMIT]: 0, // in MB
[LICENSE_QUOTAS.FLOWS_LIMIT]: 0, [LICENSE_QUOTAS.FLOWS_LIMIT]: 0,
[LICENSE_QUOTAS.USERS_LIMIT]: 0, [LICENSE_QUOTAS.USERS_LIMIT]: 0,
[LICENSE_QUOTAS.ADDITIONAL_SEATS_LIMIT]: 0 [LICENSE_QUOTAS.ADDITIONAL_SEATS_LIMIT]: 0
} })
const UNLIMITED_QUOTAS = { const getUnlimitedQuotas = () => ({
[LICENSE_QUOTAS.PREDICTIONS_LIMIT]: -1, [LICENSE_QUOTAS.PREDICTIONS_LIMIT]: -1,
[LICENSE_QUOTAS.STORAGE_LIMIT]: -1, [LICENSE_QUOTAS.STORAGE_LIMIT]: -1,
[LICENSE_QUOTAS.FLOWS_LIMIT]: -1, [LICENSE_QUOTAS.FLOWS_LIMIT]: -1,
[LICENSE_QUOTAS.USERS_LIMIT]: -1, [LICENSE_QUOTAS.USERS_LIMIT]: -1,
[LICENSE_QUOTAS.ADDITIONAL_SEATS_LIMIT]: -1 [LICENSE_QUOTAS.ADDITIONAL_SEATS_LIMIT]: -1
} })
export class UsageCacheManager { export class UsageCacheManager {
private cache: Cache private cache: Cache
@ -67,7 +67,7 @@ export class UsageCacheManager {
public async getSubscriptionDetails(subscriptionId: string, withoutCache: boolean = false): Promise<Record<string, any>> { public async getSubscriptionDetails(subscriptionId: string, withoutCache: boolean = false): Promise<Record<string, any>> {
const stripeManager = await StripeManager.getInstance() const stripeManager = await StripeManager.getInstance()
if (!stripeManager || !subscriptionId) { if (!stripeManager || !subscriptionId) {
return UNLIMITED_QUOTAS return getUnlimitedQuotas()
} }
// Skip cache if withoutCache is true // Skip cache if withoutCache is true
@ -90,7 +90,7 @@ export class UsageCacheManager {
public async getQuotas(subscriptionId: string, withoutCache: boolean = false): Promise<Record<string, number>> { public async getQuotas(subscriptionId: string, withoutCache: boolean = false): Promise<Record<string, number>> {
const stripeManager = await StripeManager.getInstance() const stripeManager = await StripeManager.getInstance()
if (!stripeManager || !subscriptionId) { if (!stripeManager || !subscriptionId) {
return UNLIMITED_QUOTAS return getUnlimitedQuotas()
} }
// Skip cache if withoutCache is true // Skip cache if withoutCache is true
@ -105,7 +105,7 @@ export class UsageCacheManager {
const subscription = await stripeManager.getStripe().subscriptions.retrieve(subscriptionId) const subscription = await stripeManager.getStripe().subscriptions.retrieve(subscriptionId)
const items = subscription.items.data const items = subscription.items.data
if (items.length === 0) { if (items.length === 0) {
return DISABLED_QUOTAS return getDisabledQuotas()
} }
const productId = items[0].price.product as string const productId = items[0].price.product as string
@ -113,7 +113,7 @@ export class UsageCacheManager {
const productMetadata = product.metadata const productMetadata = product.metadata
if (!productMetadata || Object.keys(productMetadata).length === 0) { if (!productMetadata || Object.keys(productMetadata).length === 0) {
return DISABLED_QUOTAS return getDisabledQuotas()
} }
const quotas: Record<string, number> = {} const quotas: Record<string, number> = {}

View File

@ -57,6 +57,14 @@ const calculatePercentage = (count, total) => {
return Math.min((count / total) * 100, 100) return Math.min((count / total) * 100, 100)
} }
const calculateTotalCredits = (grants) => {
if (!grants || !Array.isArray(grants)) return 0
return grants.reduce((total, grant) => {
const grantValue = grant?.amount?.monetary?.value || 0
return total + grantValue
}, 0)
}
const AccountSettings = () => { const AccountSettings = () => {
const theme = useTheme() const theme = useTheme()
const dispatch = useDispatch() const dispatch = useDispatch()
@ -89,6 +97,10 @@ const AccountSettings = () => {
const [occupiedSeats, setOccupiedSeats] = useState(0) const [occupiedSeats, setOccupiedSeats] = useState(0)
const [totalSeats, setTotalSeats] = useState(0) const [totalSeats, setTotalSeats] = useState(0)
const [creditsBalance, setCreditsBalance] = useState(null) const [creditsBalance, setCreditsBalance] = useState(null)
const totalCredits = useMemo(() => {
return creditsBalance ? calculateTotalCredits(creditsBalance.grants) : 0
}, [creditsBalance])
const [creditsPackages, setCreditsPackages] = useState([]) const [creditsPackages, setCreditsPackages] = useState([])
const [usageWithCredits, setUsageWithCredits] = useState(null) const [usageWithCredits, setUsageWithCredits] = useState(null)
const [openCreditsDialog, setOpenCreditsDialog] = useState(false) const [openCreditsDialog, setOpenCreditsDialog] = useState(false)
@ -837,11 +849,7 @@ const AccountSettings = () => {
<Stack sx={{ alignItems: 'center' }} flexDirection='row'> <Stack sx={{ alignItems: 'center' }} flexDirection='row'>
<Typography variant='body2'>Available Credits:</Typography> <Typography variant='body2'>Available Credits:</Typography>
<Typography sx={{ ml: 1, color: theme.palette.success.dark }} variant='h3'> <Typography sx={{ ml: 1, color: theme.palette.success.dark }} variant='h3'>
{getCreditsBalanceApi.loading ? ( {getCreditsBalanceApi.loading ? <CircularProgress size={16} /> : totalCredits || 0}
<CircularProgress size={16} />
) : (
creditsBalance?.balance || 0
)}
</Typography> </Typography>
</Stack> </Stack>
{usageWithCredits && ( {usageWithCredits && (
@ -1633,7 +1641,7 @@ const AccountSettings = () => {
Current Balance Current Balance
</Typography> </Typography>
<Typography variant='h4' color='success.main'> <Typography variant='h4' color='success.main'>
{creditsBalance?.balance || 0} Credits {totalCredits || 0} Credits
</Typography> </Typography>
</Box> </Box>