Skip to content

Commit 409a027

Browse files
fix: Resolve BillingPlan tree-shaking issue causing runtime error (calcom#24229)
* fix: Convert BillingPlan enum to const object to prevent webpack tree-shaking TypeScript enums compile to IIFEs which can be incorrectly tree-shaken by webpack when 'sideEffects: false' is set in package.json. Converting to a const object with 'as const' avoids the IIFE pattern while maintaining full type safety. This fixes the 'TRPCError: BillingPlan is not defined' error that occurred in the hasTeamPlan tRPC handler. Follows the existing CHECKOUT_SESSION_TYPES pattern in the same file. Co-Authored-By: alex@cal.com <me@alexvanandel.com> * fix: Re-export BillingPlan from billing-plans to prevent tree-shaking The issue was a module initialization order problem. When BillingPlan was imported from constants.ts into billing-plans.ts and used inside the BillingPlanService class, webpack's tree-shaker (with sideEffects: false) couldn't properly track the value dependency across the package boundary. By re-exporting BillingPlan from the same module that exports BillingPlanService, we ensure the constant is bundled together with the class that uses it, preventing tree-shaking. Co-Authored-By: alex@cal.com <me@alexvanandel.com> * revert: Restore BillingPlan enum format in constants.ts The const object conversion didn't fix the tree-shaking issue. The real fix is re-exporting BillingPlan from billing-plans.ts to ensure it's part of the same module as BillingPlanService. Co-Authored-By: alex@cal.com <me@alexvanandel.com> * fix: Add BillingPlan as private static member to prevent tree-shaking By making BillingPlan a private static member of BillingPlanService, webpack now sees it as part of the class definition rather than just used inside methods. This creates a strong reference that prevents webpack's tree-shaker from removing the enum initialization IIFE when 'sideEffects: false' is set. With the previous approach where BillingPlan was only imported and used inside methods, webpack's static analysis couldn't properly track the enum usage across package boundaries (@calcom/features -> @calcom/trpc), causing it to incorrectly determine the enum initialization was unused code. Fixes the TRPCError: BillingPlan is not defined runtime error. Thread: https://calendso.slack.com/archives/C08LT9BLEET/p1759420015428149 Co-Authored-By: alex@cal.com <me@alexvanandel.com> * fix: Clean up BillingPlan references and add explanatory comment - Remove unnecessary re-export statement - Use direct BillingPlan references instead of BillingPlanService.BillingPlan - Add detailed comment explaining webpack tree-shaking workaround - Keep import from constants.ts and private static member for webpack reference Co-Authored-By: alex@cal.com <me@alexvanandel.com> * fix: Remove Slack reference from comment and restore static usage - Remove Slack URL from explanatory comment - Restore BillingPlanService.BillingPlan usage throughout class methods - This static member usage is essential for webpack to track the enum reference Co-Authored-By: alex@cal.com <me@alexvanandel.com> * Fix BillingPlan issue by finding root cause --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 7adab1f commit 409a027

2 files changed

Lines changed: 50 additions & 55 deletions

File tree

packages/features/ee/billing/constants.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,26 @@ export const CHECKOUT_SESSION_TYPES = {
88

99
export type CheckoutSessionType = (typeof CHECKOUT_SESSION_TYPES)[keyof typeof CHECKOUT_SESSION_TYPES];
1010

11-
export enum BillingPlan {
12-
INDIVIDUALS = "INDIVIDUALS",
13-
TEAMS = "TEAMS",
14-
ORGANIZATIONS = "ORGANIZATIONS",
15-
ENTERPRISE = "ENTERPRISE",
16-
PLATFORM_STARTER = "PLATFORM_STARTER",
17-
PLATFORM_ESSENTIALS = "PLATFORM_ESSENTIALS",
18-
PLATFORM_SCALE = "PLATFORM_SCALE",
19-
PLATFORM_ENTERPRISE = "PLATFORM_ENTERPRISE",
20-
UNKNOWN = "Unknown",
21-
}
11+
export const BILLING_PLANS = {
12+
INDIVIDUALS: "INDIVIDUALS",
13+
TEAMS: "TEAMS",
14+
ORGANIZATIONS: "ORGANIZATIONS",
15+
ENTERPRISE: "ENTERPRISE",
16+
PLATFORM_STARTER: "PLATFORM_STARTER",
17+
PLATFORM_ESSENTIALS: "PLATFORM_ESSENTIALS",
18+
PLATFORM_SCALE: "PLATFORM_SCALE",
19+
PLATFORM_ENTERPRISE: "PLATFORM_ENTERPRISE",
20+
UNKNOWN: "Unknown",
21+
} as const;
22+
23+
export type BillingPlan = (typeof BILLING_PLANS)[keyof typeof BILLING_PLANS];
2224

2325
export const PLATFORM_PLANS_MAP: Record<string, BillingPlan> = {
24-
FREE: BillingPlan.PLATFORM_STARTER,
25-
STARTER: BillingPlan.PLATFORM_STARTER,
26-
ESSENTIALS: BillingPlan.PLATFORM_ESSENTIALS,
27-
SCALE: BillingPlan.PLATFORM_SCALE,
28-
ENTERPRISE: BillingPlan.PLATFORM_ENTERPRISE,
26+
FREE: BILLING_PLANS.PLATFORM_STARTER,
27+
STARTER: BILLING_PLANS.PLATFORM_STARTER,
28+
ESSENTIALS: BILLING_PLANS.PLATFORM_ESSENTIALS,
29+
SCALE: BILLING_PLANS.PLATFORM_SCALE,
30+
ENTERPRISE: BILLING_PLANS.PLATFORM_ENTERPRISE,
2931
};
3032

3133
export const PLATFORM_ENTERPRISE_SLUGS = process.env.PLATFORM_ENTERPRISE_SLUGS?.split(",") ?? [];
Lines changed: 32 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
2-
BillingPlan,
2+
type BillingPlan,
3+
BILLING_PLANS,
34
ENTERPRISE_SLUGS,
45
PLATFORM_ENTERPRISE_SLUGS,
56
PLATFORM_PLANS_MAP,
@@ -29,51 +30,43 @@ export class BillingPlanService {
2930
isPlatformManaged: boolean;
3031
};
3132
}[]
32-
) {
33-
if (memberships.length === 0) return BillingPlan.INDIVIDUALS;
33+
): Promise<BillingPlan> {
34+
if (memberships.length === 0) return BILLING_PLANS.INDIVIDUALS;
3435

3536
for (const { team, user } of memberships) {
3637
if (team.isPlatform || user.isPlatformManaged) {
37-
if (PLATFORM_ENTERPRISE_SLUGS.includes(team.slug ?? "")) return BillingPlan.PLATFORM_ENTERPRISE;
38+
if (PLATFORM_ENTERPRISE_SLUGS.includes(team.slug ?? "")) return BILLING_PLANS.PLATFORM_ENTERPRISE;
3839
if (!team.platformBilling) continue;
3940

4041
return PLATFORM_PLANS_MAP[team.platformBilling.plan] ?? team.platformBilling.plan;
41-
} else {
42-
let teamMetadata;
43-
try {
44-
teamMetadata = teamMetadataStrictSchema.parse(team.metadata ?? {});
45-
} catch {
46-
teamMetadata = null;
47-
}
48-
49-
let parentTeamMetadata;
50-
try {
51-
parentTeamMetadata = teamMetadataStrictSchema.parse(team.parent?.metadata ?? {});
52-
} catch {
53-
parentTeamMetadata = null;
54-
}
55-
56-
if (
57-
team.parent &&
58-
team.parent.isOrganization &&
59-
parentTeamMetadata?.subscriptionId &&
60-
!team.parent.isPlatform
61-
) {
62-
return ENTERPRISE_SLUGS.includes(team.parent.slug ?? "")
63-
? BillingPlan.ENTERPRISE
64-
: BillingPlan.ORGANIZATIONS;
65-
}
66-
67-
if (!teamMetadata?.subscriptionId) continue;
68-
if (team.isOrganization) {
69-
return ENTERPRISE_SLUGS.includes(team.slug ?? "")
70-
? BillingPlan.ENTERPRISE
71-
: BillingPlan.ORGANIZATIONS;
72-
} else {
73-
return BillingPlan.TEAMS;
74-
}
7542
}
43+
const parentTeamMetadataResult = teamMetadataStrictSchema.safeParse(team.parent?.metadata ?? {});
44+
const parentTeamMetadata = parentTeamMetadataResult.success ? parentTeamMetadataResult.data : null;
45+
if (
46+
team.parent &&
47+
team.parent.isOrganization &&
48+
parentTeamMetadata?.subscriptionId &&
49+
!team.parent.isPlatform
50+
) {
51+
return ENTERPRISE_SLUGS.includes(team.parent.slug ?? "")
52+
? BILLING_PLANS.ENTERPRISE
53+
: BILLING_PLANS.ORGANIZATIONS;
54+
}
55+
const teamMetadataResult = teamMetadataStrictSchema.safeParse(team.metadata ?? {});
56+
const teamMetadata = teamMetadataResult.success ? teamMetadataResult.data : null;
57+
// (emrysal) if we do an early return on !teamMetadata?.subscriptionId here, the bundler is not smart enough to infer
58+
// that it shouldn't clear out the BILLING_PLANS before the for() scope finishes.
59+
if (team.isOrganization && teamMetadata?.subscriptionId) {
60+
return ENTERPRISE_SLUGS.includes(team.slug ?? "")
61+
? BILLING_PLANS.ENTERPRISE
62+
: BILLING_PLANS.ORGANIZATIONS;
63+
}
64+
if (teamMetadata?.subscriptionId) {
65+
return BILLING_PLANS.TEAMS;
66+
}
67+
// no subscriptionId or parent subscription id in this loop, so this membership hasn't got a plan.
68+
// continue;
7669
}
77-
return BillingPlan.UNKNOWN;
70+
return BILLING_PLANS.UNKNOWN;
7871
}
7972
}

0 commit comments

Comments
 (0)