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)
|
||||
}
|
||||
|
||||
public async updateAdditionalSeats(subscriptionId: string, quantity: number, prorationDate: number) {
|
||||
public async updateAdditionalSeats(subscriptionId: string, quantity: number, prorationDate: number, increase: boolean) {
|
||||
if (!subscriptionId) return {}
|
||||
|
||||
if (!this.stripeManager) {
|
||||
|
|
@ -330,7 +330,8 @@ export class IdentityManager {
|
|||
const { success, subscription, invoice, paymentFailed, paymentError } = await this.stripeManager.updateAdditionalSeats(
|
||||
subscriptionId,
|
||||
quantity,
|
||||
prorationDate
|
||||
prorationDate,
|
||||
increase
|
||||
)
|
||||
|
||||
// Fetch product details to get quotas
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import { Request } from 'express'
|
|||
import { UsageCacheManager } from './UsageCacheManager'
|
||||
import { UserPlan } from './Interface'
|
||||
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 {
|
||||
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) {
|
||||
throw new Error('Stripe is not initialized')
|
||||
}
|
||||
|
||||
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
|
||||
const prices = await this.stripe.prices.list({
|
||||
product: process.env.ADDITIONAL_SEAT_ID,
|
||||
active: true,
|
||||
limit: 1
|
||||
})
|
||||
|
||||
if (prices.data.length === 0) {
|
||||
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
|
||||
const adjustedProrationDate = this.calculateSafeProrationDate(subscription)
|
||||
|
||||
// Create an invoice immediately for the proration
|
||||
const updatedSubscription = await this.stripe.subscriptions.update(subscriptionId, {
|
||||
const subscriptionUpdateData: any = {
|
||||
items: [
|
||||
additionalSeatsItem
|
||||
? {
|
||||
|
|
@ -396,10 +409,16 @@ export class StripeManager {
|
|||
price: prices.data[0].id,
|
||||
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
|
||||
const invoice = await this.stripe.invoices.list({
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ export class OrganizationController {
|
|||
|
||||
public async updateAdditionalSeats(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { subscriptionId, quantity, prorationDate } = req.body
|
||||
const { subscriptionId, quantity, prorationDate, increase } = req.body
|
||||
if (!subscriptionId) {
|
||||
return res.status(400).json({ error: 'Subscription ID is required' })
|
||||
}
|
||||
|
|
@ -144,8 +144,10 @@ export class OrganizationController {
|
|||
if (!prorationDate) {
|
||||
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 result = await identityManager.updateAdditionalSeats(subscriptionId, quantity, prorationDate)
|
||||
const result = await identityManager.updateAdditionalSeats(subscriptionId, quantity, prorationDate, increase)
|
||||
|
||||
return res.status(StatusCodes.OK).json(result)
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ const getAdditionalSeatsQuantity = (subscriptionId) =>
|
|||
const getCustomerDefaultSource = (customerId) => client.get(`/organization/customer-default-source?customerId=${customerId}`)
|
||||
const getAdditionalSeatsProration = (subscriptionId, quantity) =>
|
||||
client.get(`/organization/additional-seats-proration?subscriptionId=${subscriptionId}&quantity=${quantity}`)
|
||||
const updateAdditionalSeats = (subscriptionId, quantity, prorationDate) =>
|
||||
client.post(`/organization/update-additional-seats`, { subscriptionId, quantity, prorationDate })
|
||||
const updateAdditionalSeats = (subscriptionId, quantity, prorationDate, increase) =>
|
||||
client.post(`/organization/update-additional-seats`, { subscriptionId, quantity, prorationDate, increase })
|
||||
const getPlanProration = (subscriptionId, newPlanId) =>
|
||||
client.get(`/organization/plan-proration?subscriptionId=${subscriptionId}&newPlanId=${newPlanId}`)
|
||||
const updateSubscriptionPlan = (subscriptionId, newPlanId, prorationDate) =>
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@ const AccountSettings = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const handleSeatsModification = async (newSeatsAmount) => {
|
||||
const handleSeatsModification = async (newSeatsAmount, increase) => {
|
||||
try {
|
||||
setIsUpdatingSeats(true)
|
||||
|
||||
|
|
@ -358,7 +358,8 @@ const AccountSettings = () => {
|
|||
const response = await updateAdditionalSeatsApi.request(
|
||||
currentUser?.activeOrganizationSubscriptionId,
|
||||
newSeatsAmount,
|
||||
prorationInfo.prorationDate
|
||||
prorationInfo.prorationDate,
|
||||
increase
|
||||
)
|
||||
|
||||
// Check if payment failed but seats were updated (Issue #4 fix)
|
||||
|
|
@ -1181,7 +1182,7 @@ const AccountSettings = () => {
|
|||
</Button>
|
||||
<Button
|
||||
variant='outlined'
|
||||
onClick={() => handleSeatsModification(purchasedSeats - seatsQuantity)}
|
||||
onClick={() => handleSeatsModification(purchasedSeats - seatsQuantity, false)}
|
||||
disabled={
|
||||
getCustomerDefaultSourceApi.loading ||
|
||||
!getCustomerDefaultSourceApi.data ||
|
||||
|
|
@ -1435,7 +1436,7 @@ const AccountSettings = () => {
|
|||
</Button>
|
||||
<Button
|
||||
variant='contained'
|
||||
onClick={() => handleSeatsModification(seatsQuantity + purchasedSeats)}
|
||||
onClick={() => handleSeatsModification(seatsQuantity + purchasedSeats, true)}
|
||||
disabled={
|
||||
getCustomerDefaultSourceApi.loading ||
|
||||
!getCustomerDefaultSourceApi.data ||
|
||||
|
|
|
|||
Loading…
Reference in New Issue