Fix issues when purchasing credits
This commit is contained in:
parent
051d8ef401
commit
dbf5a2edc5
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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> = {}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue