Skip to content

Conversation

@Amit91848
Copy link
Contributor

What does this PR do?

  • Fixes #XXXX (GitHub issue number)
  • Fixes CAL-XXXX (Linear issue number - should be visible at the bottom of the GitHub issue description)

Video Demo (if applicable):

Screen.Recording.2025-09-12.at.4.58.52.PM.mov

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 12, 2025

Walkthrough

Adds a billing plan determination flow and propagates it to client and Intercom. Introduces BillingPlan enum, platform plan map, and enterprise slug env vars. Implements BillingPlanService.getUserPlanByMemberships using membership data (including platform and metadata parsing). Adds MembershipRepository.findAllMembershipsByUserIdForBilling to fetch required fields. Updates hasTeamPlan handler to return { hasTeamPlan, plan }. Extends useHasPaidPlan hook to return plan. Passes Plan to Intercom in both web Intercom hook and support conversation API when creating contacts. Updates turbo.json to expose ENTERPRISE_SLUGS and PLATFORM_ENTERPRISE_SLUGS. Minor non-functional formatting changes.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title succinctly and accurately describes the main change — adding user plan information to useHasPaidPlan for Intercom integration — and maps directly to the modified hook and Intercom usage in the changeset. It is concise, specific, and avoids noise or vague wording, making the intent clear to reviewers scanning history. Therefore it reflects the primary change introduced by the PR.
Description Check ✅ Passed The PR description states the purpose (adding user plan info for Intercom), includes a demo link and mandatory checklist, and is clearly related to the code changes described in the summary; it is not off-topic. Although it contains placeholder issue references and lacks implementation details, it meets the lenient requirement of being related to the changeset.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/user_plan_for_intercom

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@graphite-app graphite-app bot requested a review from a team September 12, 2025 11:44
@keithwillcode keithwillcode added the core area: core, team members only label Sep 12, 2025
@dosubot dosubot bot added billing area: billing, stripe, payments, paypal, get paid ✨ feature New feature or request labels Sep 12, 2025
@vercel
Copy link

vercel bot commented Sep 12, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
cal Ignored Ignored Sep 24, 2025 3:18pm
cal-eu Ignored Ignored Sep 24, 2025 3:18pm

@@ -0,0 +1,46 @@
import { BillingPlans, ENTERPRISE_SLUGS, PLATFORM_ENTERPRISE_SLUGS } from "@calcom/ee/billing/constants";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be tests added around this logic to verify its correctness


if (memberships.length === 0) return BillingPlans.INDIVIDUALS;

for (const { team, user } of memberships) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this logic to a different class that takes in the data it uses. It should live in a folder inside of /billing called /domain. That way you can easily write unit tests for it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The service is ok to keep and have it read data from the repository, but then it should pass the data to a domain-level class/function for the logic-only piece

@github-actions github-actions bot added the ❗️ migrations contains migration files label Sep 15, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Sep 15, 2025

E2E results are ready!

…lows

- Add unit tests for BillingPlanService.getUserPlanByMemberships() covering all plan determination scenarios
- Add tests for team creation handler verifying TEAMS vs ORGANIZATIONS plan assignment
- Add tests for hasTeamPlan handler integration with BillingPlanService
- Add tests for MembershipRepository.findAllMembershipsByUserIdForBilling() data fetching
- Add tests for InternalTeamBilling upgrade/downgrade flows with proper mocking
- All tests follow existing vitest patterns with proper Prisma and service mocking
- Covers both self-serve and platform billing scenarios with comprehensive edge cases

Co-Authored-By: [email protected] <[email protected]>
@Amit91848 Amit91848 marked this pull request as ready for review September 15, 2025 14:14
@Amit91848 Amit91848 requested a review from a team as a code owner September 15, 2025 14:14
@dosubot dosubot bot added platform Anything related to our platform plan teams area: teams, round robin, collective, managed event-types labels Sep 15, 2025
Copy link
Contributor

@volnei volnei left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some small comments

Comment on lines 317 to 318
static async findAllMembershipsByUserIdForBilling({ userId }: { userId: number }) {
return await prisma.membership.findMany({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to be static and no need the await here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@github-actions github-actions bot marked this pull request as draft September 23, 2025 16:49
WEBAPP @map("webapp")
}

enum Plans {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistency is key, Plans should be "Plan" and all enum keys should be singular.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed for enum Name. But enum keys are plural in intercom, so for proper mapping and worfklows in intercom, kept it plural here as well.

@keithwillcode
Copy link
Contributor

@Amit91848 we were chatting and decided that we are going to involve @joeauyeung in a bigger overhaul of billing instead of just tacking this on quickly.

For intercom, let’s build a daily sync using the query I provided (adding filters for dates)

@keithwillcode
Copy link
Contributor

Chatting with @Amit91848 some more FYI @joeauyeung and we think a good middleground is to use the runtime code built here without adding the new plan column to the DB so we can still properly kick off intercom sessions based on plan.

@Amit91848 Amit91848 marked this pull request as ready for review September 24, 2025 15:16
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
turbo.json (1)

25-36: Deduplicate repeated env keys in globalEnv

There are duplicates that add noise and confusion:

  • CALCOM_CREDENTIAL_SYNC_SECRET (Line 26 and Line 35)
  • INTERCOM_SECRET (Line 90 and Line 91)
  • ORG_MONTHLY_CREDITS (Line 124 and Line 193)
  • VAPID_PRIVATE_KEY (Line 254 and Line 260)

Recommend removing duplicates in this PR or a follow-up.

Also applies to: 90-92, 124-124, 193-193, 254-261

packages/features/ee/billing/domain/billing-plans.ts (3)

11-12: Remove unnecessary async and annotate return type

Method has no awaits. Make it synchronous and explicitly typed for clarity.

-export class BillingPlanService {
-  async getUserPlanByMemberships(
+export class BillingPlanService {
+  getUserPlanByMemberships(
     memberships: {
       team: {
         isOrganization: boolean;
         isPlatform: boolean;
         slug: string | null;
         metadata: JsonValue;
         parent: {
           isOrganization: boolean;
           slug: string | null;
           isPlatform: boolean;
           metadata: JsonValue;
         } | null;
         platformBilling: {
           plan: string;
         } | null;
       };
       user: {
         isPlatformManaged: boolean;
       };
     }[]
-  ) {
+  ): BillingPlan {

Also applies to: 32-33


35-76: Membership order affects outcome; choose highest-precedence plan instead

Current early returns make the result depend on array order. Prefer computing the best plan across all memberships (e.g., PLATFORM_ENTERPRISE > ENTERPRISE > PLATFORM_SCALE > ORGANIZATIONS > TEAMS > PLATFORM_ESSENTIALS > PLATFORM_STARTER > INDIVIDUALS > UNKNOWN).

If you want, I can provide a concrete refactor that accumulates a candidate per membership using a rank map and returns the highest-ranked plan.


33-33: Fallback on no match: UNKNOWN vs INDIVIDUALS

You return INDIVIDUALS when memberships is empty, but UNKNOWN when memberships exist yet don’t match. Consider consistently defaulting to INDIVIDUALS, or document the distinction.

Also applies to: 77-78

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 06ebb5c and 3184309.

📒 Files selected for processing (7)
  • apps/web/app/api/support/conversation/route.ts (3 hunks)
  • packages/features/ee/billing/constants.ts (1 hunks)
  • packages/features/ee/billing/domain/billing-plans.ts (1 hunks)
  • packages/lib/server/repository/membership.ts (2 hunks)
  • packages/trpc/server/routers/viewer/organizations/publish.handler.ts (1 hunks)
  • packages/trpc/server/routers/viewer/teams/hasTeamPlan.handler.ts (2 hunks)
  • turbo.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/trpc/server/routers/viewer/organizations/publish.handler.ts
  • packages/lib/server/repository/membership.ts
  • apps/web/app/api/support/conversation/route.ts
  • packages/features/ee/billing/constants.ts
  • packages/trpc/server/routers/viewer/teams/hasTeamPlan.handler.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/features/ee/billing/domain/billing-plans.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/features/ee/billing/domain/billing-plans.ts
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/features/ee/billing/domain/billing-plans.ts
🧬 Code graph analysis (1)
packages/features/ee/billing/domain/billing-plans.ts (2)
packages/features/ee/billing/constants.ts (3)
  • PLATFORM_ENTERPRISE_SLUGS (31-31)
  • PLATFORM_PLANS_MAP (23-29)
  • ENTERPRISE_SLUGS (32-32)
packages/prisma/zod-utils.ts (1)
  • teamMetadataStrictSchema (389-405)
🔇 Additional comments (2)
turbo.json (1)

284-287: LGTM: exposing enterprise slug envs to Turbo

These belong in globalEnv so changes invalidate cache appropriately. Please also add them to .env.example and developer docs.

packages/features/ee/billing/domain/billing-plans.ts (1)

36-41: Confirm platform gating via user.isPlatformManaged

Using user.isPlatformManaged to enter the platform path even when team.isPlatform is false can classify non-platform teams as platform based on the user flag. Verify this is intentional.

Comment on lines +40 to +41
return PLATFORM_PLANS_MAP[team.platformBilling.plan] ?? team.platformBilling.plan;
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Keep return type strictly BillingPlan (don’t return raw strings)

Fallback currently returns a string which breaks type guarantees and can leak unknown values downstream.

Apply this diff:

-        return PLATFORM_PLANS_MAP[team.platformBilling.plan] ?? team.platformBilling.plan;
+        const planKey = team.platformBilling.plan?.toUpperCase?.() ?? "";
+        return PLATFORM_PLANS_MAP[planKey] ?? BillingPlan.UNKNOWN;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return PLATFORM_PLANS_MAP[team.platformBilling.plan] ?? team.platformBilling.plan;
} else {
const planKey = team.platformBilling.plan?.toUpperCase?.() ?? "";
return PLATFORM_PLANS_MAP[planKey] ?? BillingPlan.UNKNOWN;
} else {
🤖 Prompt for AI Agents
In packages/features/ee/billing/domain/billing-plans.ts around lines 40-41, the
fallback currently returns the raw string team.platformBilling.plan which breaks
the function's BillingPlan return type; replace the raw-string fallback with a
strict BillingPlan value by adding a small type-guard isBillingPlan(value: any):
value is BillingPlan that checks Object.values(BillingPlan).includes(value),
then return PLATFORM_PLANS_MAP[team.platformBilling.plan] if defined, otherwise
if isBillingPlan(team.platformBilling.plan) return team.platformBilling.plan,
else throw a descriptive error (or return an explicit safe BillingPlan default
if your codebase prefers) so the function never returns an untyped string.

@keithwillcode keithwillcode removed the ❗️ migrations contains migration files label Sep 24, 2025
@volnei volnei self-requested a review September 24, 2025 16:16
@Amit91848 Amit91848 enabled auto-merge (squash) September 24, 2025 16:38
@Amit91848 Amit91848 dismissed stale reviews from emrysal and keithwillcode September 25, 2025 07:30

stale

@Amit91848 Amit91848 merged commit 2ed0380 into main Sep 25, 2025
65 of 67 checks passed
@Amit91848 Amit91848 deleted the feat/user_plan_for_intercom branch September 25, 2025 07:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

billing area: billing, stripe, payments, paypal, get paid core area: core, team members only ✨ feature New feature or request platform Anything related to our platform plan ready-for-e2e size/L teams area: teams, round robin, collective, managed event-types

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants