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 { InternalFlowiseError } from './errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes'
import logger from './utils/logger'
export class StripeManager {
private static instance: StripeManager
@ -826,7 +827,10 @@ export class StripeManager {
const creditBalance = await this.stripe!.billing.creditBalanceSummary.retrieve({
customer: customerId,
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>> {
if (!this.stripe) {
throw new Error('Stripe is not initialized')
}
try {
if (!process.env.CREDIT_PRODUCT_ID) {
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')
}
const subscription = await this.stripe!.subscriptions.retrieve(subscriptionId)
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId)
const customerId = subscription.customer as string
if (!customerId) {
@ -869,28 +877,40 @@ export class StripeManager {
// Get credit packages
const packages = await this.getCreditsPackages()
logger.info(`Retrieved credit packages: ${JSON.stringify(packages)}`)
const selectedPackage = packages.find((pkg: any) => pkg.id === packageType)
logger.info(`Selected credit packages: ${JSON.stringify(selectedPackage)}`)
if (!selectedPackage) {
throw new Error(`No active price found for ${packageType} package`)
}
// Create invoice for credit purchase
const invoiceItem = await this.stripe!.invoiceItems.create({
customer: customerId,
amount: selectedPackage.price,
currency: 'usd',
description: `${selectedPackage.credits} Credits Package`
})
const invoice = await this.stripe!.invoices.create({
const invoice = await this.stripe.invoices.create({
customer: customerId,
auto_advance: true,
collection_method: 'charge_automatically'
})
const finalizedInvoice = await this.stripe!.invoices.finalizeInvoice(invoice.id!)
const paidInvoice = await this.stripe!.invoices.pay(finalizedInvoice.id!)
if (!invoice.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') {
throw new Error('Payment failed')
@ -899,8 +919,11 @@ export class StripeManager {
// Add metered subscription item if it doesn't exist
const meteredItemResult = await this.addMeteredSubscriptionItem(subscriptionId)
// Get price
const price = await this.stripe.prices.retrieve(process.env.METERED_PRICE_ID!)
// Create credit grant
const creditGrant = await this.stripe!.billing.creditGrants.create({
const creditGrant = await this.stripe.billing.creditGrants.create({
customer: customerId,
amount: {
type: 'monetary' as any,
@ -911,8 +934,7 @@ export class StripeManager {
},
applicability_config: {
scope: {
price_type: 'metered',
prices: [process.env.METERED_PRICE_ID!]
prices: [{ id: price.id }]
}
} as any,
category: 'paid',
@ -951,7 +973,10 @@ export class StripeManager {
const creditBalance = await this.stripe!.billing.creditBalanceSummary.retrieve({
customer: customerId,
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 }> {
if (!this.stripe) {
throw new Error('Stripe is not initialized')
}
try {
if (!process.env.METERED_PRICE_ID) {
throw new Error('METERED_PRICE_ID environment variable is required')
@ -1071,7 +1100,7 @@ export class StripeManager {
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
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
await this.stripe!.subscriptionItems.create({
await this.stripe.subscriptionItems.create({
subscription: subscriptionId,
price: process.env.METERED_PRICE_ID!,
quantity: 0
price: process.env.METERED_PRICE_ID!
})
return {

View File

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

View File

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