feat: not allow to add seats when there're unsuccessful additional seat payment
This commit is contained in:
parent
ba71c2975e
commit
9ae54bc921
|
|
@ -321,7 +321,7 @@ export class IdentityManager {
|
||||||
return await this.stripeManager.getAdditionalSeatsProration(subscriptionId, newQuantity)
|
return await this.stripeManager.getAdditionalSeatsProration(subscriptionId, newQuantity)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateAdditionalSeats(subscriptionId: string, quantity: number, prorationDate: number) {
|
public async updateAdditionalSeats(subscriptionId: string, quantity: number, prorationDate: number, increase: boolean) {
|
||||||
if (!subscriptionId) return {}
|
if (!subscriptionId) return {}
|
||||||
|
|
||||||
if (!this.stripeManager) {
|
if (!this.stripeManager) {
|
||||||
|
|
@ -330,7 +330,8 @@ export class IdentityManager {
|
||||||
const { success, subscription, invoice, paymentFailed, paymentError } = await this.stripeManager.updateAdditionalSeats(
|
const { success, subscription, invoice, paymentFailed, paymentError } = await this.stripeManager.updateAdditionalSeats(
|
||||||
subscriptionId,
|
subscriptionId,
|
||||||
quantity,
|
quantity,
|
||||||
prorationDate
|
prorationDate,
|
||||||
|
increase
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fetch product details to get quotas
|
// Fetch product details to get quotas
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ import { Request } from 'express'
|
||||||
import { UsageCacheManager } from './UsageCacheManager'
|
import { UsageCacheManager } from './UsageCacheManager'
|
||||||
import { UserPlan } from './Interface'
|
import { UserPlan } from './Interface'
|
||||||
import { LICENSE_QUOTAS } from './utils/constants'
|
import { LICENSE_QUOTAS } from './utils/constants'
|
||||||
|
import { InternalFlowiseError } from './errors/internalFlowiseError'
|
||||||
|
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
|
||||||
|
|
@ -359,33 +362,43 @@ export class StripeManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateAdditionalSeats(subscriptionId: string, quantity: number, _prorationDate: number) {
|
public async updateAdditionalSeats(subscriptionId: string, quantity: number, _prorationDate: number, increase: boolean) {
|
||||||
if (!this.stripe) {
|
if (!this.stripe) {
|
||||||
throw new Error('Stripe is not initialized')
|
throw new Error('Stripe is not initialized')
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId)
|
|
||||||
const additionalSeatsItem = subscription.items.data.find(
|
|
||||||
(item) => (item.price.product as string) === process.env.ADDITIONAL_SEAT_ID
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get the price ID for additional seats if needed
|
// Get the price ID for additional seats if needed
|
||||||
const prices = await this.stripe.prices.list({
|
const prices = await this.stripe.prices.list({
|
||||||
product: process.env.ADDITIONAL_SEAT_ID,
|
product: process.env.ADDITIONAL_SEAT_ID,
|
||||||
active: true,
|
active: true,
|
||||||
limit: 1
|
limit: 1
|
||||||
})
|
})
|
||||||
|
|
||||||
if (prices.data.length === 0) {
|
if (prices.data.length === 0) {
|
||||||
throw new Error('No active price found for additional seats')
|
throw new Error('No active price found for additional seats')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openInvoices = await this.stripe.invoices.list({
|
||||||
|
subscription: subscriptionId,
|
||||||
|
status: 'open'
|
||||||
|
})
|
||||||
|
const openAdditionalSeatsInvoices = openInvoices.data.filter((invoice) =>
|
||||||
|
invoice.lines?.data?.some((line) => line.price?.id === prices.data[0].id)
|
||||||
|
)
|
||||||
|
logger.info(`openAdditionalSeatsInvoices: ${openAdditionalSeatsInvoices.length}, increase: ${increase}`)
|
||||||
|
if (openAdditionalSeatsInvoices.length > 0 && increase === true)
|
||||||
|
throw new InternalFlowiseError(StatusCodes.PAYMENT_REQUIRED, "Not allow to add seats when there're unsuccessful payment")
|
||||||
|
|
||||||
|
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId)
|
||||||
|
const additionalSeatsItem = subscription.items.data.find(
|
||||||
|
(item) => (item.price.product as string) === process.env.ADDITIONAL_SEAT_ID
|
||||||
|
)
|
||||||
|
|
||||||
// TODO: Fix proration date for sandbox testing - use subscription period bounds
|
// TODO: Fix proration date for sandbox testing - use subscription period bounds
|
||||||
const adjustedProrationDate = this.calculateSafeProrationDate(subscription)
|
const adjustedProrationDate = this.calculateSafeProrationDate(subscription)
|
||||||
|
|
||||||
// Create an invoice immediately for the proration
|
// Create an invoice immediately for the proration
|
||||||
const updatedSubscription = await this.stripe.subscriptions.update(subscriptionId, {
|
const subscriptionUpdateData: any = {
|
||||||
items: [
|
items: [
|
||||||
additionalSeatsItem
|
additionalSeatsItem
|
||||||
? {
|
? {
|
||||||
|
|
@ -396,10 +409,16 @@ export class StripeManager {
|
||||||
price: prices.data[0].id,
|
price: prices.data[0].id,
|
||||||
quantity: quantity
|
quantity: quantity
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
proration_behavior: 'always_invoice',
|
}
|
||||||
proration_date: adjustedProrationDate
|
if (openAdditionalSeatsInvoices.length > 0) {
|
||||||
})
|
await this.stripe.invoices.voidInvoice(openAdditionalSeatsInvoices[0].id)
|
||||||
|
subscriptionUpdateData.proration_behavior = 'none'
|
||||||
|
} else {
|
||||||
|
;(subscriptionUpdateData.proration_behavior = 'always_invoice'),
|
||||||
|
(subscriptionUpdateData.proration_date = adjustedProrationDate)
|
||||||
|
}
|
||||||
|
const updatedSubscription = await this.stripe.subscriptions.update(subscriptionId, subscriptionUpdateData)
|
||||||
|
|
||||||
// Get the latest invoice for this subscription
|
// Get the latest invoice for this subscription
|
||||||
const invoice = await this.stripe.invoices.list({
|
const invoice = await this.stripe.invoices.list({
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ export class OrganizationController {
|
||||||
|
|
||||||
public async updateAdditionalSeats(req: Request, res: Response, next: NextFunction) {
|
public async updateAdditionalSeats(req: Request, res: Response, next: NextFunction) {
|
||||||
try {
|
try {
|
||||||
const { subscriptionId, quantity, prorationDate } = req.body
|
const { subscriptionId, quantity, prorationDate, increase } = req.body
|
||||||
if (!subscriptionId) {
|
if (!subscriptionId) {
|
||||||
return res.status(400).json({ error: 'Subscription ID is required' })
|
return res.status(400).json({ error: 'Subscription ID is required' })
|
||||||
}
|
}
|
||||||
|
|
@ -144,8 +144,10 @@ export class OrganizationController {
|
||||||
if (!prorationDate) {
|
if (!prorationDate) {
|
||||||
return res.status(400).json({ error: 'Proration date is required' })
|
return res.status(400).json({ error: 'Proration date is required' })
|
||||||
}
|
}
|
||||||
|
if (increase === undefined) return res.status(StatusCodes.BAD_REQUEST).json({ error: 'Increase is required' })
|
||||||
|
|
||||||
const identityManager = getRunningExpressApp().identityManager
|
const identityManager = getRunningExpressApp().identityManager
|
||||||
const result = await identityManager.updateAdditionalSeats(subscriptionId, quantity, prorationDate)
|
const result = await identityManager.updateAdditionalSeats(subscriptionId, quantity, prorationDate, increase)
|
||||||
|
|
||||||
return res.status(StatusCodes.OK).json(result)
|
return res.status(StatusCodes.OK).json(result)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ const getAdditionalSeatsQuantity = (subscriptionId) =>
|
||||||
const getCustomerDefaultSource = (customerId) => client.get(`/organization/customer-default-source?customerId=${customerId}`)
|
const getCustomerDefaultSource = (customerId) => client.get(`/organization/customer-default-source?customerId=${customerId}`)
|
||||||
const getAdditionalSeatsProration = (subscriptionId, quantity) =>
|
const getAdditionalSeatsProration = (subscriptionId, quantity) =>
|
||||||
client.get(`/organization/additional-seats-proration?subscriptionId=${subscriptionId}&quantity=${quantity}`)
|
client.get(`/organization/additional-seats-proration?subscriptionId=${subscriptionId}&quantity=${quantity}`)
|
||||||
const updateAdditionalSeats = (subscriptionId, quantity, prorationDate) =>
|
const updateAdditionalSeats = (subscriptionId, quantity, prorationDate, increase) =>
|
||||||
client.post(`/organization/update-additional-seats`, { subscriptionId, quantity, prorationDate })
|
client.post(`/organization/update-additional-seats`, { subscriptionId, quantity, prorationDate, increase })
|
||||||
const getPlanProration = (subscriptionId, newPlanId) =>
|
const getPlanProration = (subscriptionId, newPlanId) =>
|
||||||
client.get(`/organization/plan-proration?subscriptionId=${subscriptionId}&newPlanId=${newPlanId}`)
|
client.get(`/organization/plan-proration?subscriptionId=${subscriptionId}&newPlanId=${newPlanId}`)
|
||||||
const updateSubscriptionPlan = (subscriptionId, newPlanId, prorationDate) =>
|
const updateSubscriptionPlan = (subscriptionId, newPlanId, prorationDate) =>
|
||||||
|
|
|
||||||
|
|
@ -347,7 +347,7 @@ const AccountSettings = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSeatsModification = async (newSeatsAmount) => {
|
const handleSeatsModification = async (newSeatsAmount, increase) => {
|
||||||
try {
|
try {
|
||||||
setIsUpdatingSeats(true)
|
setIsUpdatingSeats(true)
|
||||||
|
|
||||||
|
|
@ -358,7 +358,8 @@ const AccountSettings = () => {
|
||||||
const response = await updateAdditionalSeatsApi.request(
|
const response = await updateAdditionalSeatsApi.request(
|
||||||
currentUser?.activeOrganizationSubscriptionId,
|
currentUser?.activeOrganizationSubscriptionId,
|
||||||
newSeatsAmount,
|
newSeatsAmount,
|
||||||
prorationInfo.prorationDate
|
prorationInfo.prorationDate,
|
||||||
|
increase
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if payment failed but seats were updated (Issue #4 fix)
|
// Check if payment failed but seats were updated (Issue #4 fix)
|
||||||
|
|
@ -1181,7 +1182,7 @@ const AccountSettings = () => {
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
onClick={() => handleSeatsModification(purchasedSeats - seatsQuantity)}
|
onClick={() => handleSeatsModification(purchasedSeats - seatsQuantity, false)}
|
||||||
disabled={
|
disabled={
|
||||||
getCustomerDefaultSourceApi.loading ||
|
getCustomerDefaultSourceApi.loading ||
|
||||||
!getCustomerDefaultSourceApi.data ||
|
!getCustomerDefaultSourceApi.data ||
|
||||||
|
|
@ -1435,7 +1436,7 @@ const AccountSettings = () => {
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='contained'
|
variant='contained'
|
||||||
onClick={() => handleSeatsModification(seatsQuantity + purchasedSeats)}
|
onClick={() => handleSeatsModification(seatsQuantity + purchasedSeats, true)}
|
||||||
disabled={
|
disabled={
|
||||||
getCustomerDefaultSourceApi.loading ||
|
getCustomerDefaultSourceApi.loading ||
|
||||||
!getCustomerDefaultSourceApi.data ||
|
!getCustomerDefaultSourceApi.data ||
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue