Skip to content

Fix stripe-related issues #4584

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 40 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
4d52643
Add stripe webhook for handling payment failures
0xi4o Jun 5, 2025
71cbe60
Remove unnecessary readme file
0xi4o Jun 5, 2025
7d3c070
Refactor stripe webhooks handler
0xi4o Jun 12, 2025
32ade38
Fix merge conflict
0xi4o Jun 12, 2025
260219e
feature/organization-status (#4637)
chungyau97 Jun 12, 2025
aaf2f6e
Update behavior for stripe webhooks
0xi4o Jun 13, 2025
407c8bb
Merge branch 'fix/stripe-issues' of github.com:FlowiseAI/Flowise into…
0xi4o Jun 13, 2025
4a2ea0a
Enforce restrictions based on organization.status (#4652)
chungyau97 Jun 13, 2025
cbcda55
Merge branch 'main' of github.com:FlowiseAI/Flowise into fix/stripe-i…
0xi4o Jun 16, 2025
e596c4a
Show dialog in UI for suspended organization
0xi4o Jun 16, 2025
81c8d42
Merge branch 'fix/stripe-issues' of github.com:FlowiseAI/Flowise into…
0xi4o Jun 16, 2025
52e2dab
Fix org status check when receiving invoice.paid event from stripe
0xi4o Jun 16, 2025
d1f71ce
Update behavior for invoice.paid event based on organization status
0xi4o Jun 16, 2025
2f87f64
Lint fix
0xi4o Jun 16, 2025
9a41eff
Fix issue with subscription not resuming after paying uncollectible i…
0xi4o Jun 17, 2025
36c872f
Fix issue with updating last login for org members when subscription …
0xi4o Jun 18, 2025
764cc6c
Fix first month free callout styles in dark mode
0xi4o Jun 18, 2025
6d39b83
Fix issues with downgrading to free plan
0xi4o Jun 19, 2025
324868a
Fix issue with upgrading with invalid payment method
0xi4o Jun 24, 2025
6ace661
Fix issues and code cleanup
0xi4o Jun 27, 2025
fb64ea7
Merge branch 'main' of github.com:FlowiseAI/Flowise into fix/stripe-i…
0xi4o Jun 27, 2025
3b1c79f
Merge branch 'main' of github.com:FlowiseAI/Flowise into fix/stripe-i…
0xi4o Jul 3, 2025
fda7ca5
Add logout and contact support button to org suspended dialog
0xi4o Jul 4, 2025
611b312
Add logout button and contact support link in account suspended dialog
0xi4o Jul 7, 2025
5e25ce5
Fix issues in stripe
0xi4o Jul 7, 2025
18d2e0f
Show errors in payment when upgrading plans or purchasing additional …
0xi4o Jul 8, 2025
cc6931e
Only show org suspended dialog to org admins
0xi4o Jul 8, 2025
56c51ac
fix: list invoice with invalid status
chungyau97 Jul 8, 2025
c74e175
Merge branch 'main' into fix/stripe-issues
chungyau97 Jul 9, 2025
deae7d9
feat: switch member to own workspace after switching to past-due org
chungyau97 Jul 10, 2025
da8623d
Merge branch 'main' into fix/stripe-issues
chungyau97 Jul 10, 2025
46dc432
fix: member always get redirect to own workspace
chungyau97 Jul 10, 2025
ba71c29
Merge branch 'main' into fix/stripe-issues
chungyau97 Jul 11, 2025
9ae54bc
feat: not allow to add seats when there're unsuccessful additional se…
chungyau97 Jul 11, 2025
77f738e
Merge branch 'main' into fix/stripe-issues
chungyau97 Jul 14, 2025
7150c54
fix: void incorrect invoice in updateSubscriptionPlan
chungyau97 Jul 14, 2025
268763c
feat: handle invoices with both paid and unpaid items in updateAdditi…
chungyau97 Jul 14, 2025
784b98a
Merge branch 'main' into fix/stripe-issues
chungyau97 Jul 14, 2025
99f7f7d
fix: incorrect additional seats quantity of mix invoices
chungyau97 Jul 14, 2025
370c55a
Merge branch 'main' into fix/stripe-issues
chungyau97 Jul 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 132 additions & 67 deletions packages/server/src/IdentityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { LoginMethodStatus } from './enterprise/database/entities/login-method.e
import { ErrorMessage, LoggedInUser } from './enterprise/Interface.Enterprise'
import { Permissions } from './enterprise/rbac/Permissions'
import { LoginMethodService } from './enterprise/services/login-method.service'
import { Organization, OrganizationStatus } from './enterprise/database/entities/organization.entity'
import { OrganizationService } from './enterprise/services/organization.service'
import Auth0SSO from './enterprise/sso/Auth0SSO'
import AzureSSO from './enterprise/sso/AzureSSO'
Expand Down Expand Up @@ -320,13 +321,18 @@ 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) {
throw new Error('Stripe manager is not initialized')
}
const { success, subscription, invoice } = await this.stripeManager.updateAdditionalSeats(subscriptionId, quantity, prorationDate)
const { success, subscription, invoice, paymentFailed, paymentError } = await this.stripeManager.updateAdditionalSeats(
subscriptionId,
quantity,
prorationDate,
increase
)

// Fetch product details to get quotas
const items = subscription.items.data
Expand Down Expand Up @@ -358,7 +364,13 @@ export class IdentityManager {
subsriptionDetails: this.stripeManager.getSubscriptionObject(subscription)
})

return { success, subscription, invoice }
return {
success,
subscription,
invoice,
paymentFailed,
paymentError: paymentFailed ? paymentError?.message || 'Payment failed' : null
}
}

public async getPlanProration(subscriptionId: string, newPlanId: string) {
Expand All @@ -379,85 +391,138 @@ export class IdentityManager {
if (!req.user) {
throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, GeneralErrorMessage.UNAUTHORIZED)
}
const { success, subscription } = await this.stripeManager.updateSubscriptionPlan(subscriptionId, newPlanId, prorationDate)
if (success) {
// Fetch product details to get quotas
const product = await this.stripeManager.getStripe().products.retrieve(newPlanId)
const productMetadata = product.metadata

// Extract quotas from metadata
const quotas: Record<string, number> = {}
for (const key in productMetadata) {
if (key.startsWith('quota:')) {
quotas[key] = parseInt(productMetadata[key])
try {
const result = await this.stripeManager.updateSubscriptionPlan(subscriptionId, newPlanId, prorationDate)
const { success, subscription, special_case, paymentFailed, paymentError } = result
if (success) {
// Handle special case: downgrade from past_due to free plan
if (special_case === 'downgrade_from_past_due') {
// Update organization status to active using OrganizationService
const queryRunner = getRunningExpressApp().AppDataSource.createQueryRunner()
await queryRunner.connect()

try {
const organizationService = new OrganizationService()

// Find organization by subscriptionId
const organization = await queryRunner.manager.findOne(Organization, {
where: { subscriptionId }
})

if (organization) {
await organizationService.updateOrganization(
{
id: organization.id,
status: OrganizationStatus.ACTIVE,
updatedBy: req.user.id
},
queryRunner,
true // fromStripe = true to allow status updates
)
}
} finally {
await queryRunner.release()
}
}
}

const additionalSeatsItem = subscription.items.data.find(
(item) => (item.price.product as string) === process.env.ADDITIONAL_SEAT_ID
)
quotas[LICENSE_QUOTAS.ADDITIONAL_SEATS_LIMIT] = additionalSeatsItem?.quantity || 0
// Fetch product details to get quotas
const product = await this.stripeManager.getStripe().products.retrieve(newPlanId)
const productMetadata = product.metadata

// Get features from Stripe
const features = await this.getFeaturesByPlan(subscription.id, true)
// Extract quotas from metadata
const quotas: Record<string, number> = {}
for (const key in productMetadata) {
if (key.startsWith('quota:')) {
quotas[key] = parseInt(productMetadata[key])
}
}

// Update the cache with new subscription data including quotas
const cacheManager = await UsageCacheManager.getInstance()
const additionalSeatsItem = subscription.items.data.find(
(item: any) => (item.price.product as string) === process.env.ADDITIONAL_SEAT_ID
)
quotas[LICENSE_QUOTAS.ADDITIONAL_SEATS_LIMIT] = additionalSeatsItem?.quantity || 0

const updateCacheData: Record<string, any> = {
features,
quotas,
subsriptionDetails: this.stripeManager.getSubscriptionObject(subscription)
}
// Get features from Stripe
const features = await this.getFeaturesByPlan(subscription.id, true)

if (
newPlanId === process.env.CLOUD_FREE_ID ||
newPlanId === process.env.CLOUD_STARTER_ID ||
newPlanId === process.env.CLOUD_PRO_ID
) {
updateCacheData.productId = newPlanId
}
// Update the cache with new subscription data including quotas
const cacheManager = await UsageCacheManager.getInstance()

await cacheManager.updateSubscriptionDataToCache(subscriptionId, updateCacheData)
const updateCacheData: Record<string, any> = {
features,
quotas,
subsriptionDetails: this.stripeManager.getSubscriptionObject(subscription)
}

const loggedInUser: LoggedInUser = {
...req.user,
activeOrganizationSubscriptionId: subscription.id,
features
}
if (
newPlanId === process.env.CLOUD_FREE_ID ||
newPlanId === process.env.CLOUD_STARTER_ID ||
newPlanId === process.env.CLOUD_PRO_ID
) {
updateCacheData.productId = newPlanId
}

if (
newPlanId === process.env.CLOUD_FREE_ID ||
newPlanId === process.env.CLOUD_STARTER_ID ||
newPlanId === process.env.CLOUD_PRO_ID
) {
loggedInUser.activeOrganizationProductId = newPlanId
}
await cacheManager.updateSubscriptionDataToCache(subscriptionId, updateCacheData)

req.user = {
...req.user,
...loggedInUser
}
const loggedInUser: LoggedInUser = {
...req.user,
activeOrganizationSubscriptionId: subscription.id,
features
}

// Update passport session
// @ts-ignore
req.session.passport.user = {
...req.user,
...loggedInUser
}
if (
newPlanId === process.env.CLOUD_FREE_ID ||
newPlanId === process.env.CLOUD_STARTER_ID ||
newPlanId === process.env.CLOUD_PRO_ID
) {
loggedInUser.activeOrganizationProductId = newPlanId
}

req.session.save((err) => {
if (err) throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, GeneralErrorMessage.UNHANDLED_EDGE_CASE)
})
req.user = {
...req.user,
...loggedInUser
}

// Update passport session
// @ts-ignore
req.session.passport.user = {
...req.user,
...loggedInUser
}

req.session.save((err) => {
if (err) throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, GeneralErrorMessage.UNHANDLED_EDGE_CASE)
})

return {
status: 'success',
user: loggedInUser,
paymentFailed,
paymentError: paymentFailed ? paymentError?.message || 'Payment failed' : null
}
}
return {
status: 'success',
user: loggedInUser
status: 'error',
message: 'Payment or subscription update not completed'
}
}
return {
status: 'error',
message: 'Payment or subscription update not completed'
} catch (error: any) {
// Enhanced error handling for payment method failures
if (error.type === 'StripeCardError' || error.code === 'card_declined') {
throw new InternalFlowiseError(
StatusCodes.PAYMENT_REQUIRED,
'Your payment method was declined. Please update your payment method and try again.'
)
}

if (error.type === 'StripeInvalidRequestError' && error.message?.includes('payment_method')) {
throw new InternalFlowiseError(
StatusCodes.PAYMENT_REQUIRED,
'There was an issue with your payment method. Please update your payment method and try again.'
)
}

// Re-throw other errors
throw error
}
}

Expand Down
Loading