Skip to content

Commit 2d799b3

Browse files
fix(billing): plan should be detected from stripe subscription object (#3090)
* fix(billing): plan should be detected from stripe subscription object * fix typing
1 parent 92403e0 commit 2d799b3

File tree

2 files changed

+97
-9
lines changed

2 files changed

+97
-9
lines changed

apps/sim/lib/auth/auth.ts

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
ensureOrganizationForTeamSubscription,
3131
syncSubscriptionUsageLimits,
3232
} from '@/lib/billing/organization'
33-
import { getPlans } from '@/lib/billing/plans'
33+
import { getPlans, resolvePlanFromStripeSubscription } from '@/lib/billing/plans'
3434
import { syncSeatsFromStripeQuantity } from '@/lib/billing/validation/seat-management'
3535
import { handleChargeDispute, handleDisputeClosed } from '@/lib/billing/webhooks/disputes'
3636
import { handleManualEnterpriseSubscription } from '@/lib/billing/webhooks/enterprise'
@@ -2641,29 +2641,42 @@ export const auth = betterAuth({
26412641
}
26422642
},
26432643
onSubscriptionComplete: async ({
2644+
stripeSubscription,
26442645
subscription,
26452646
}: {
26462647
event: Stripe.Event
26472648
stripeSubscription: Stripe.Subscription
26482649
subscription: any
26492650
}) => {
2651+
const { priceId, planFromStripe, isTeamPlan } =
2652+
resolvePlanFromStripeSubscription(stripeSubscription)
2653+
26502654
logger.info('[onSubscriptionComplete] Subscription created', {
26512655
subscriptionId: subscription.id,
26522656
referenceId: subscription.referenceId,
2653-
plan: subscription.plan,
2657+
dbPlan: subscription.plan,
2658+
planFromStripe,
2659+
priceId,
26542660
status: subscription.status,
26552661
})
26562662

2663+
const subscriptionForOrgCreation = isTeamPlan
2664+
? { ...subscription, plan: 'team' }
2665+
: subscription
2666+
26572667
let resolvedSubscription = subscription
26582668
try {
2659-
resolvedSubscription = await ensureOrganizationForTeamSubscription(subscription)
2669+
resolvedSubscription = await ensureOrganizationForTeamSubscription(
2670+
subscriptionForOrgCreation
2671+
)
26602672
} catch (orgError) {
26612673
logger.error(
26622674
'[onSubscriptionComplete] Failed to ensure organization for team subscription',
26632675
{
26642676
subscriptionId: subscription.id,
26652677
referenceId: subscription.referenceId,
2666-
plan: subscription.plan,
2678+
dbPlan: subscription.plan,
2679+
planFromStripe,
26672680
error: orgError instanceof Error ? orgError.message : String(orgError),
26682681
stack: orgError instanceof Error ? orgError.stack : undefined,
26692682
}
@@ -2684,22 +2697,67 @@ export const auth = betterAuth({
26842697
event: Stripe.Event
26852698
subscription: any
26862699
}) => {
2700+
const stripeSubscription = event.data.object as Stripe.Subscription
2701+
const { priceId, planFromStripe, isTeamPlan } =
2702+
resolvePlanFromStripeSubscription(stripeSubscription)
2703+
2704+
if (priceId && !planFromStripe) {
2705+
logger.warn(
2706+
'[onSubscriptionUpdate] Could not determine plan from Stripe price ID',
2707+
{
2708+
subscriptionId: subscription.id,
2709+
priceId,
2710+
dbPlan: subscription.plan,
2711+
}
2712+
)
2713+
}
2714+
2715+
const isUpgradeToTeam =
2716+
isTeamPlan &&
2717+
subscription.plan !== 'team' &&
2718+
!subscription.referenceId.startsWith('org_')
2719+
2720+
const effectivePlanForTeamFeatures = planFromStripe ?? subscription.plan
2721+
26872722
logger.info('[onSubscriptionUpdate] Subscription updated', {
26882723
subscriptionId: subscription.id,
26892724
status: subscription.status,
2690-
plan: subscription.plan,
2725+
dbPlan: subscription.plan,
2726+
planFromStripe,
2727+
isUpgradeToTeam,
2728+
referenceId: subscription.referenceId,
26912729
})
26922730

2731+
const subscriptionForOrgCreation = isUpgradeToTeam
2732+
? { ...subscription, plan: 'team' }
2733+
: subscription
2734+
26932735
let resolvedSubscription = subscription
26942736
try {
2695-
resolvedSubscription = await ensureOrganizationForTeamSubscription(subscription)
2737+
resolvedSubscription = await ensureOrganizationForTeamSubscription(
2738+
subscriptionForOrgCreation
2739+
)
2740+
2741+
if (isUpgradeToTeam) {
2742+
logger.info(
2743+
'[onSubscriptionUpdate] Detected Pro -> Team upgrade, ensured organization creation',
2744+
{
2745+
subscriptionId: subscription.id,
2746+
originalPlan: subscription.plan,
2747+
newPlan: planFromStripe,
2748+
resolvedReferenceId: resolvedSubscription.referenceId,
2749+
}
2750+
)
2751+
}
26962752
} catch (orgError) {
26972753
logger.error(
26982754
'[onSubscriptionUpdate] Failed to ensure organization for team subscription',
26992755
{
27002756
subscriptionId: subscription.id,
27012757
referenceId: subscription.referenceId,
2702-
plan: subscription.plan,
2758+
dbPlan: subscription.plan,
2759+
planFromStripe,
2760+
isUpgradeToTeam,
27032761
error: orgError instanceof Error ? orgError.message : String(orgError),
27042762
stack: orgError instanceof Error ? orgError.stack : undefined,
27052763
}
@@ -2717,9 +2775,8 @@ export const auth = betterAuth({
27172775
})
27182776
}
27192777

2720-
if (resolvedSubscription.plan === 'team') {
2778+
if (effectivePlanForTeamFeatures === 'team') {
27212779
try {
2722-
const stripeSubscription = event.data.object as Stripe.Subscription
27232780
const quantity = stripeSubscription.items?.data?.[0]?.quantity || 1
27242781

27252782
const result = await syncSeatsFromStripeQuantity(

apps/sim/lib/billing/plans.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type Stripe from 'stripe'
12
import {
23
getFreeTierLimit,
34
getProTierLimit,
@@ -56,10 +57,40 @@ export function getPlanByName(planName: string): BillingPlan | undefined {
5657
return getPlans().find((plan) => plan.name === planName)
5758
}
5859

60+
/**
61+
* Get a specific plan by Stripe price ID
62+
*/
63+
export function getPlanByPriceId(priceId: string): BillingPlan | undefined {
64+
return getPlans().find((plan) => plan.priceId === priceId)
65+
}
66+
5967
/**
6068
* Get plan limits for a given plan name
6169
*/
6270
export function getPlanLimits(planName: string): number {
6371
const plan = getPlanByName(planName)
6472
return plan?.limits.cost ?? getFreeTierLimit()
6573
}
74+
75+
export interface StripePlanResolution {
76+
priceId: string | undefined
77+
planFromStripe: string | null
78+
isTeamPlan: boolean
79+
}
80+
81+
/**
82+
* Resolve plan information from a Stripe subscription object.
83+
* Used to get the authoritative plan from Stripe rather than relying on DB state.
84+
*/
85+
export function resolvePlanFromStripeSubscription(
86+
stripeSubscription: Stripe.Subscription
87+
): StripePlanResolution {
88+
const priceId = stripeSubscription?.items?.data?.[0]?.price?.id
89+
const plan = priceId ? getPlanByPriceId(priceId) : undefined
90+
91+
return {
92+
priceId,
93+
planFromStripe: plan?.name ?? null,
94+
isTeamPlan: plan?.name === 'team',
95+
}
96+
}

0 commit comments

Comments
 (0)