diff --git a/packages/server/src/IdentityManager.ts b/packages/server/src/IdentityManager.ts index 09e9199c4..d7b26597e 100644 --- a/packages/server/src/IdentityManager.ts +++ b/packages/server/src/IdentityManager.ts @@ -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 diff --git a/packages/server/src/StripeManager.ts b/packages/server/src/StripeManager.ts index dabc2519e..5c80b7260 100644 --- a/packages/server/src/StripeManager.ts +++ b/packages/server/src/StripeManager.ts @@ -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({ diff --git a/packages/server/src/enterprise/controllers/organization.controller.ts b/packages/server/src/enterprise/controllers/organization.controller.ts index fc57184dd..de11fa816 100644 --- a/packages/server/src/enterprise/controllers/organization.controller.ts +++ b/packages/server/src/enterprise/controllers/organization.controller.ts @@ -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) { diff --git a/packages/ui/src/api/user.js b/packages/ui/src/api/user.js index 86165ec9c..cc5ff2992 100644 --- a/packages/ui/src/api/user.js +++ b/packages/ui/src/api/user.js @@ -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) => diff --git a/packages/ui/src/views/account/index.jsx b/packages/ui/src/views/account/index.jsx index 2236e1aed..cd4a57679 100644 --- a/packages/ui/src/views/account/index.jsx +++ b/packages/ui/src/views/account/index.jsx @@ -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 = () => {