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

View File

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

View File

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

View File

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

View File

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