feat: not allow to add seats when there're unsuccessful additional seat payment

This commit is contained in:
chungyau97 2025-07-12 02:26:37 +08:00
parent ba71c2975e
commit 9ae54bc921
5 changed files with 45 additions and 22 deletions

View File

@ -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

View File

@ -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({

View File

@ -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) {

View File

@ -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) =>

View File

@ -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 ||