Skip to content

Conversation

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

Visual Demo (For contributors especially)

A visual demonstration is strongly recommended, for both the original and new change (video / image - any one).

Video Demo (if applicable):

  • Show screen recordings of the issue or feature.
  • Demonstrate how to reproduce the issue, the behavior before and after the change.

Image Demo (if applicable):

  • Add side-by-side screenshots of the original and updated change.
  • Highlight any significant change(s).

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.

How should this be tested?

  • Are there environment variables that should be set?
  • What are the minimal test data to have?
  • What is expected (happy path) to have (input and output)?
  • Any other important info that could help to test that PR

Checklist

  • I haven't read the contributing guide
  • My code doesn't follow the style guidelines of this project
  • I haven't commented my code, particularly in hard-to-understand areas
  • I haven't checked if my changes generate no new warnings

@anikdhabal anikdhabal requested a review from a team August 25, 2025 11:17
@vercel
Copy link

vercel bot commented Aug 25, 2025

@anikdhabal is attempting to deploy a commit to the cal Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 25, 2025

Walkthrough

A new baseTeamMetadataSchema was introduced to centralize team metadata fields (including billingPeriod). The public teamMetadataSchema now derives from it via partial().nullable(), preserving optional-nullable fields. A new export, teamMetadataStrictSchema, extends the base and adds prefix validations for subscriptionId (sub_) and subscriptionItemId (si_), both nullable. Multiple modules replaced usage of teamMetadataSchema with teamMetadataStrictSchema for parsing, safeParse, and unwrap. One method type signature changed to use z.infer for existingMetadata in OrganizationRepository.updateStripeSubscriptionDetails. No other control-flow or exported signatures changed.

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@graphite-app graphite-app bot requested a review from a team August 25, 2025 11:17
@keithwillcode keithwillcode added the core area: core, team members only label Aug 25, 2025
@dosubot dosubot bot added the billing area: billing, stripe, payments, paypal, get paid label Aug 25, 2025
@graphite-app
Copy link

graphite-app bot commented Aug 25, 2025

Graphite Automations

"Add consumer team as reviewer" took an action on this PR • (08/25/25)

1 reviewer was added to this PR based on Keith Williams's automation.

"Add ready-for-e2e label" took an action on this PR • (08/26/25)

1 label was added to this PR based on Keith Williams's automation.

@anikdhabal anikdhabal requested a review from emrysal August 25, 2025 11:20
@emrysal emrysal enabled auto-merge (squash) August 25, 2025 11:21
@anikdhabal anikdhabal changed the title fix: team metadata restriction for stripe fix: team metadata restriction for subscription Aug 25, 2025
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
packages/features/ee/teams/lib/payments.test.ts (5)

545-546: Fix misuse of .rejects with a function — pass the Promise and await

rejects expects a Promise, not a function. This test can yield false positives/negatives.

-    expect(() => getTeamWithPaymentMetadata(team.id)).rejects.toThrow();
+    await expect(getTeamWithPaymentMetadata(team.id)).rejects.toThrow();

559-560: Fix misuse of .rejects with a function — pass the Promise and await

Repeat of the issue above.

-    expect(() => getTeamWithPaymentMetadata(team.id)).rejects.toThrow();
+    await expect(getTeamWithPaymentMetadata(team.id)).rejects.toThrow();

573-574: Fix misuse of .rejects with a function — pass the Promise and await

Repeat of the issue above.

-    expect(() => getTeamWithPaymentMetadata(team.id)).rejects.toThrow();
+    await expect(getTeamWithPaymentMetadata(team.id)).rejects.toThrow();

688-709: Nested Promise.all prevents awaiting user creation; flatten the array

Promise.all([ array.map(async ...) ]) passes a single element (an array of Promises) into Promise.all, which resolves immediately and does not await the inner Promises. This can introduce test flakiness and race conditions.

Apply:

-  await Promise.all([
-    Array(membersInTeam)
-      .fill(0)
-      .map(async (_, index) => {
-        return await prismock.membership.create({
-          data: {
-            team: {
-              connect: {
-                id: organization.id,
-              },
-            },
-            user: {
-              create: {
-                name: "ABC",
-                email: `test-${index}@example.com`,
-              },
-            },
-            role: "MEMBER",
-          },
-        });
-      }),
-  ]);
+  await Promise.all(
+    Array.from({ length: membersInTeam }, (_, index) =>
+      prismock.membership.create({
+        data: {
+          team: { connect: { id: organization.id } },
+          user: { create: { name: "ABC", email: `test-${index}@example.com` } },
+          role: "MEMBER",
+        },
+      })
+    )
+  );

39-43: Fix the mocked module path to match the production import

The test currently mocks @calcom/lib/constant (singular), but production code imports from @calcom/lib/constants (plural), so the mock isn’t applied and tests may use the real value unexpectedly.

• File: packages/features/ee/teams/lib/payments.test.ts
• Lines: 39–43

- vi.mock("@calcom/lib/constant", () => {
-   return {
-     MINIMUM_NUMBER_OF_ORG_SEATS: 30,
-   };
- });
+ vi.mock("@calcom/lib/constants", () => ({
+   MINIMUM_NUMBER_OF_ORG_SEATS: 30,
+ }));
🧹 Nitpick comments (2)
packages/prisma/zod-utils.ts (1)

380-391: Optional: Simplify Stripe ID prefix checks using z.string().startsWith

We’ve confirmed that packages/prisma is on Zod v^3.22.4, which includes the string().startsWith sugar method. You can replace the existing refine calls with a more concise API:

• File: packages/prisma/zod-utils.ts
Lines: ~380–391

Proposed diff:

-    subscriptionId: z
-      .string()
-      .refine((val) => val.startsWith("sub_"), {
-        message: "subscriptionId must start with 'sub_'",
-      })
-      .nullable(),
+    subscriptionId: z
+      .string()
+      .startsWith("sub_", "subscriptionId must start with 'sub_'")
+      .nullable(),

-    subscriptionItemId: z
-      .string()
-      .refine((val) => val.startsWith("si_"), {
-        message: "subscriptionItemId must start with 'si_'",
-      })
-      .nullable(),
+    subscriptionItemId: z
+      .string()
+      .startsWith("si_", "subscriptionItemId must start with 'si_'")
+      .nullable(),

This keeps null‐safety intact and aligns with the pattern used in packages/features/ee/teams/lib/payments.ts.

packages/features/ee/teams/lib/payments.test.ts (1)

742-747: Nit: simplify mock implementations with mockResolvedValue

The explicit new Promise((resolve) => resolve(data)) wrappers are verbose.

-return vi.mocked(stripe.checkout.sessions.create).mockImplementation(
-  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-  //@ts-ignore
-  async () => new Promise((resolve) => resolve(data))
-);
+return vi.mocked(stripe.checkout.sessions.create).mockResolvedValue(
+  // @ts-ignore
+  data
+);

Apply similarly to other mocks (prices.create, prices.retrieve, subscriptions.retrieve, subscriptions.update) for brevity.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • 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 efca905 and 0d8633b.

📒 Files selected for processing (3)
  • packages/features/ee/teams/lib/payments.test.ts (11 hunks)
  • packages/features/ee/teams/lib/payments.ts (1 hunks)
  • packages/prisma/zod-utils.ts (1 hunks)
🧰 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/prisma/zod-utils.ts
  • packages/features/ee/teams/lib/payments.ts
  • packages/features/ee/teams/lib/payments.test.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/prisma/zod-utils.ts
  • packages/features/ee/teams/lib/payments.ts
  • packages/features/ee/teams/lib/payments.test.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/prisma/zod-utils.ts
  • packages/features/ee/teams/lib/payments.ts
  • packages/features/ee/teams/lib/payments.test.ts
🧬 Code graph analysis (1)
packages/features/ee/teams/lib/payments.test.ts (1)
packages/features/ee/teams/lib/payments.ts (1)
  • getTeamWithPaymentMetadata (271-279)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Tests / Unit
  • GitHub Check: Type check / check-types
🔇 Additional comments (5)
packages/features/ee/teams/lib/payments.ts (1)

24-26: Good hardening: enforce Stripe ID prefixes at parse-time

Validating subscriptionId and subscriptionItemId with string().startsWith is aligned with Stripe’s ID semantics and avoids accidental misconfiguration. Using orgSeats directly from teamMetadataSchema.unwrap().shape.orgSeats keeps type parity with the shared schema. Looks good.

Please ensure the repo uses a Zod version that supports string().startsWith. If not, fall back to the refine style used in zod-utils.ts.

packages/features/ee/teams/lib/payments.test.ts (4)

341-343: IDs updated to valid Stripe prefixes — good

Switching test fixtures to sub_ and si_ prefixed IDs matches the new validation and real Stripe formats.


387-389: Consistent prefixed IDs — good

Maintains consistency across scenarios; mirrors production expectations.


437-439: Consistent prefixed IDs — good

Matches the schema contract and avoids false negatives.


483-485: Consistent prefixed IDs — good

Continues to align with the schema requirements.

@anikdhabal anikdhabal requested a review from a team August 25, 2025 13:53
@anikdhabal anikdhabal requested a review from a team as a code owner August 25, 2025 13:53
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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/api/v2/src/modules/teams/teams/teams.repository.ts (1)

88-96: Fix null deref and error ordering in setDefaultConferencingApp.

  • You parse metadata before checking team existence; parse(undefined) throws a ZodError instead of the intended NotFoundException.
  • typeof null === "object", so spreading ...teamMetadata will throw when teamMetadata is null.

Also, the NotFoundException message says “user not found” in a team repository.

Apply this diff:

   async setDefaultConferencingApp(teamId: number, appSlug?: string, appLink?: string) {
-    const team = await this.getById(teamId);
-    const teamMetadata = teamMetadataStrictSchema.parse(team?.metadata);
-
-    if (!team) {
-      throw new NotFoundException("user not found");
-    }
+    const team = await this.getById(teamId);
+    if (!team) {
+      throw new NotFoundException("team not found");
+    }
+    const teamMetadata = teamMetadataStrictSchema.parse(team.metadata ?? null);
@@
-      data: {
-        metadata:
-          typeof teamMetadata === "object"
-            ? {
-                ...teamMetadata,
-                defaultConferencingApp: {
-                  appSlug: appSlug,
-                  appLink: appLink,
-                },
-              }
-            : {},
-      },
+      data: {
+        metadata: {
+          ...(teamMetadata ?? {}),
+          defaultConferencingApp: { appSlug, appLink },
+        },
+      },
packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.ts (1)

347-353: Enforce non-throwing metadata parsing in onboarding flow

The call to teamMetadataStrictSchema.parse(…) on line 347 of
packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.ts will throw on any legacy or malformed metadata (e.g. missing “sub_” prefix), causing webhook-driven organization creation to hard-fail. This path must remain backward-compatible.

• File: packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.ts
Line: 347

Apply this refactor:

-  const existingMetadata = teamMetadataStrictSchema.parse(organization.metadata);
+  const metadataParse = teamMetadataStrictSchema.safeParse(organization.metadata);
+  if (!metadataParse.success) {
+    log.warn(
+      "Legacy/invalid organization metadata during Stripe subscription update; proceeding without blocking.",
+      safeStringify({ orgId: organization.id, error: metadataParse.error.message })
+    );
+  }
+  const existingMetadata = metadataParse.success ? metadataParse.data : null;

Additionally, your grep scan reveals numerous other direct .parse calls on team/organization metadata schemas. Audit any webhook-driven or background tasks that must tolerate legacy records, replacing blocking .parse with safeParse + graceful fallback where appropriate.

packages/trpc/server/routers/viewer/organizations/publish.handler.ts (1)

25-31: Prisma: replace include with select/_count to follow guidelines and reduce payload.

Avoid include per repo guideline. You only need id, metadata, and members count.

-  const prevTeam = await prisma.team.findUnique({
-    where: {
-      id: orgId,
-    },
-    include: { members: true },
-  });
+  const prevTeam = await prisma.team.findUnique({
+    where: { id: orgId },
+    select: {
+      id: true,
+      metadata: true,
+      _count: { select: { members: true } },
+    },
+  });
@@
-      seatsUsed: prevTeam.members.length,
+      seatsUsed: prevTeam._count.members,

Also applies to: 41-49

🧹 Nitpick comments (17)
apps/web/app/api/teams/[team]/upgrade/route.ts (4)

53-56: Validation now enforces Stripe id prefixes (nullable when absent).

Using teamMetadataStrictSchema.safeParse(prevTeam.metadata) is appropriate. Two suggestions:

  • Consider surfacing Zod error details in logs for faster triage.
  • If legacy teams may carry non-prefixed subscription ids, decide whether you want a hard 400 here or a soft migration path.

Apply this diff to add debug logging without altering the response:

-      metadata = teamMetadataStrictSchema.safeParse(prevTeam.metadata);
-      if (!metadata.success) {
+      metadata = teamMetadataStrictSchema.safeParse(prevTeam.metadata);
+      if (!metadata.success) {
+        // Optional: log issues to help debug bad payloads
+        console.debug("Invalid team metadata", metadata.error.flatten());
         throw new HttpError({ statusCode: 400, message: "Invalid team metadata" });
       }

65-67: Guard access to subscription.items.data[0].id.

Stripe guarantees at least one item for a typical subscription, but defensive checks avoid runtime errors (and make 5xx root-causes clearer) if a plan misconfiguration ever slips through.

Apply this diff:

-            subscriptionId: subscription.id || null,
-            subscriptionItemId: subscription.items.data[0].id || null,
+            subscriptionId: subscription?.id || null,
+            subscriptionItemId: subscription?.items?.data?.[0]?.id ?? null,

Optionally hard-fail if the item is unexpectedly missing:

+      if (!subscription?.items?.data?.[0]?.id) {
+        throw new HttpError({ statusCode: 500, message: "Subscription item missing for checkout session" });
+      }

44-47: Prisma: select only fields you need.

Per repo guidelines, prefer select over broad fetches. Here you only use id, slug, isOrganization, and metadata.

-    let team = await prisma.team.findFirst({
-      where: { metadata: { path: ["paymentId"], equals: checkoutSession.id } },
-    });
+    let team = await prisma.team.findFirst({
+      where: { metadata: { path: ["paymentId"], equals: checkoutSession.id } },
+      select: { id: true, slug: true, isOrganization: true, metadata: true },
+    });
@@
-      const prevTeam = await prisma.team.findFirstOrThrow({ where: { id } });
+      const prevTeam = await prisma.team.findFirstOrThrow({
+        where: { id },
+        select: { id: true, slug: true, metadata: true },
+      });
@@
-    const redirectUrl = team?.isOrganization
+    const redirectUrl = team?.isOrganization
       ? `${WEBAPP_URL}/settings/organizations/profile?upgraded=true`
       : `${WEBAPP_URL}/settings/teams/${team.id}/profile?upgraded=true`;

Also applies to: 59-69, 95-98


82-87: Redundant re-parse of metadata.

The second safeParse result isn’t used. If the purpose is to assert validity for existing teams, consider logging failures and continuing, or remove the block to simplify.

-    if (!metadata) {
-      metadata = teamMetadataStrictSchema.safeParse(team.metadata);
-      if (!metadata.success) {
-        throw new HttpError({ statusCode: 400, message: "Invalid team metadata" });
-      }
-    }
+    // Optional: re-validate existing metadata here if you need a hard assertion.
packages/prisma/zod-utils.ts (2)

375-393: Solid consolidation via baseTeamMetadataSchema; confirm unknown-key policy.

Centralizing fields (incl. billingPeriod) is a win. Note: z.object(...) without .passthrough() strips unknown keys. If previous teamMetadataSchema allowed unknowns, this will now silently drop them during parse/serialize flows.

If you want to preserve unknown keys:

-const baseTeamMetadataSchema = z.object({
+const baseTeamMetadataSchema = z
+  .object({
     defaultConferencingApp: schemaDefaultConferencingApp.optional(),
     requestedSlug: z.string().or(z.null()),
     paymentId: z.string(),
     subscriptionId: z.string().nullable(),
     subscriptionItemId: z.string().nullable(),
     orgSeats: z.number().nullable(),
     orgPricePerSeat: z.number().nullable(),
     migratedToOrgFrom: z
       .object({
         teamSlug: z.string().or(z.null()).optional(),
         lastMigrationTime: z.string().optional(),
         reverted: z.boolean().optional(),
         lastRevertTime: z.string().optional(),
       })
       .optional(),
     billingPeriod: z.nativeEnum(BillingPeriod).optional(),
-});
+})
+  .passthrough();

If stripping is intentional, please confirm to avoid data loss surprises in downstream merges.


394-413: Prefix constraints look correct; keep them nullable and optional.

The refine checks enforce “sub_”/“si_” only when values are present; null is still allowed, which matches usage. Consider extracting the prefixes to named constants to avoid string drift.

+const STRIPE_SUB_PREFIX = "sub_";
+const STRIPE_SI_PREFIX = "si_";
 export const teamMetadataStrictSchema = baseTeamMetadataSchema
   .extend({
     subscriptionId: z
       .string()
-      .refine((val) => val.startsWith("sub_"), {
+      .refine((val) => val.startsWith(STRIPE_SUB_PREFIX), {
         message: "subscriptionId must start with 'sub_'",
       })
       .nullable(),
     subscriptionItemId: z
       .string()
-      .refine((val) => val.startsWith("si_"), {
+      .refine((val) => val.startsWith(STRIPE_SI_PREFIX), {
         message: "subscriptionItemId must start with 'si_'",
       })
       .nullable(),
   })
   .partial()
   .nullable();
apps/api/v2/src/modules/teams/teams/teams.repository.ts (1)

18-22: Optional: narrow Prisma selections.

These reads return full Team rows but callers only need a subset. Trim with select to reduce payloads per guidelines.

Example:

-  async getById(teamId: number) {
-    return this.dbRead.prisma.team.findUnique({
-      where: { id: teamId },
-    });
-  }
+  async getById(teamId: number) {
+    return this.dbRead.prisma.team.findUnique({
+      where: { id: teamId },
+      select: { id: true, metadata: true }, // extend as needed per caller
+    });
+  }

Also applies to: 63-73

apps/web/playwright/lib/orgMigration.ts (3)

12-12: Importing both schemas is fine; keep usage consistent.

If the goal is to only constrain Stripe fields at write/upgrade boundaries, prefer teamMetadataSchema in test helpers unless you explicitly want failures on legacy data.


591-601: Strict parse may reject legacy teams; consider safe fallback.

dbRemoveTeamFromOrg only needs migratedToOrgFrom.teamSlug. Failing due to unrelated subscriptionId/ItemId prefixes could make org removal brittle.

Apply this diff to prefer strict, with graceful fallback to the relaxed schema:

-  const teamMetadata = teamMetadataStrictSchema.parse(team?.metadata);
+  const parsed = teamMetadataStrictSchema.safeParse(team?.metadata);
+  const teamMetadata = parsed.success
+    ? parsed.data
+    : teamMetadataSchema.parse(team?.metadata);

257-265: Use select instead of include per guidelines.

Both getTeamOrThrowError and the update that returns members use include. Switch to select to fetch only what you need.

   const team = await prisma.team.findUnique({
     where: { id: targetOrgId },
-    include: {
-      organizationSettings: true,
-    },
+    select: {
+      id: true,
+      slug: true,
+      metadata: true,
+      isOrganization: true,
+      organizationSettings: true,
+    },
   });
@@
-      include: {
-        members: true,
-      },
+      select: {
+        id: true,
+        slug: true,
+        parentId: true,
+        members: true, // optionally narrow member fields if needed
+      },

Also applies to: 608-611

packages/features/ee/billing/teams/internal-team-billing.ts (1)

50-56: Minor: log after validating subscriptionId.

We currently log “Cancelling subscription null …” if subscriptionId is missing. Move the info log below the guard for cleaner logs.

-      const { subscriptionId } = this.team.metadata;
-      log.info(`Cancelling subscription ${subscriptionId} for team ${this.team.id}`);
-      if (!subscriptionId) throw Error("missing subscriptionId");
+      const { subscriptionId } = this.team.metadata;
+      if (!subscriptionId) throw Error("missing subscriptionId");
+      log.info(`Cancelling subscription ${subscriptionId} for team ${this.team.id}`);
packages/trpc/server/routers/viewer/teams/update.handler.ts (1)

98-105: Ensure requestedSlug is dropped even if strict parsing fails.

Strict validation can fail on legacy metadata; in that case we currently keep requestedSlug around, which can cause stale metadata. Fall back to removing requestedSlug from raw JSON when strict parsing fails.

-    const metadataParse = teamMetadataStrictSchema.safeParse(prevTeam.metadata);
-    if (metadataParse.success) {
-      const { requestedSlug: _, ...cleanMetadata } = metadataParse.data || {};
-      data.metadata = {
-        ...cleanMetadata,
-      };
-    }
+    const metadataParse = teamMetadataStrictSchema.safeParse(prevTeam.metadata);
+    if (metadataParse.success) {
+      const { requestedSlug: _, ...cleanMetadata } = metadataParse.data || {};
+      data.metadata = { ...cleanMetadata };
+    } else if (prevTeam.metadata && typeof prevTeam.metadata === "object" && !Array.isArray(prevTeam.metadata)) {
+      // Fallback for legacy metadata that fails strict validation: still drop requestedSlug.
+      const { requestedSlug: _rs, ...rest } = (prevTeam.metadata as Record<string, unknown>);
+      data.metadata = rest as unknown as Prisma.JsonValue;
+    }
packages/trpc/server/routers/viewer/organizations/update.schema.ts (1)

31-31: Confirm unwrap/shape semantics and unknown key policy.

teamMetadataStrictSchema is partial().nullable(); unwrap() will drop only the nullable, preserving partial keys. Verify whether the underlying base object is strict or passthrough. If you intend to retain unknown keys on writes, ensure the base schema uses .passthrough(); if you prefer dropping unknown keys, current setup is fine. Please confirm desired behavior before rollout.

Would you like me to prepare a brief doc blurb for /docs outlining the new validation rules (sub_/si_ prefixes) and how unknown keys are handled?

packages/trpc/server/routers/viewer/organizations/update.handler.ts (1)

168-171: Optional: fail early on invalid metadata

Defense-in-depth: validate input.metadata against the strict schema (after the omit above) and return a typed TRPC error if invalid, rather than letting deeper layers surface errors.

Apply this diff (assuming the omit refactor above):

-  const publicTeamMetadataSchema = teamMetadataStrictSchema
+  const publicTeamMetadataSchema = teamMetadataStrictSchema
     .unwrap()
     .omit({ subscriptionId: true, subscriptionItemId: true });
+  const parsedPublicMetadata = publicTeamMetadataSchema.safeParse(input.metadata ?? {});
+  if (!parsedPublicMetadata.success) {
+    throw new TRPCError({ code: "BAD_REQUEST", message: parsedPublicMetadata.error.message });
+  }

Then replace subsequent parses with parsedPublicMetadata.data.

packages/trpc/server/routers/viewer/organizations/adminUpdate.handler.ts (1)

37-37: Future-proof admin metadata merges

Currently, only requestedSlug is merged. If admin updates ever include arbitrary metadata, apply the same “omit subscription fields” pattern used for viewer routes to avoid unintended writes to Stripe IDs.

Example:

-const { mergeMetadata } = getMetadataHelpers(teamMetadataStrictSchema.unwrap(), existingOrg.metadata || {});
+const strictUnwrapped = teamMetadataStrictSchema.unwrap();
+const { mergeMetadata } = getMetadataHelpers(strictUnwrapped, existingOrg.metadata || {});
+const publicTeamMetadataSchema = strictUnwrapped.omit({ subscriptionId: true, subscriptionItemId: true });
+// When/if merging admin-provided metadata:
+// data.metadata = mergeMetadata(publicTeamMetadataSchema.parse(input.metadata ?? {}));
packages/lib/server/repository/organization.ts (2)

9-11: Type-only import is fine, but you’ll need a runtime import to validate

Since you changed the method signature to z.infer, keep this type-only import. If you adopt runtime validation in updateStripeSubscriptionDetails (recommended below), add a value import too.


452-463: No null‐spread risk detected; consider optional Stripe ID format checks

I verified that the only caller of updateStripeSubscriptionDetails in
packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.ts
uses

const existingMetadata = teamMetadataStrictSchema.parse(organization.metadata);
await OrganizationRepository.updateStripeSubscriptionDetails({});

which guarantees a non‐null, fully validated metadata object. The spread of existingMetadata inside the method cannot throw at runtime today.

Recommendations:

  • If you’d like the signature to reflect that guarantee, tighten the parameter to a non‐nullable schema type (e.g. use the strict Zod type directly).
  • Optionally add a runtime check or Zod refinement to ensure:
    stripeSubscriptionId starts with sub_
    stripeSubscriptionItemId starts with si_
    before persisting, to guard against invalid values in future call sites.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • 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 0d8633b and a2df137.

📒 Files selected for processing (13)
  • apps/api/v2/src/modules/teams/teams/teams.repository.ts (2 hunks)
  • apps/web/app/api/teams/[team]/upgrade/route.ts (3 hunks)
  • apps/web/playwright/lib/orgMigration.ts (2 hunks)
  • packages/features/ee/billing/teams/internal-team-billing.ts (1 hunks)
  • packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.ts (2 hunks)
  • packages/lib/server/repository/organization.ts (2 hunks)
  • packages/prisma/zod-utils.ts (1 hunks)
  • packages/trpc/server/routers/viewer/organizations/adminUpdate.handler.ts (2 hunks)
  • packages/trpc/server/routers/viewer/organizations/createTeams.handler.ts (2 hunks)
  • packages/trpc/server/routers/viewer/organizations/publish.handler.ts (2 hunks)
  • packages/trpc/server/routers/viewer/organizations/update.handler.ts (2 hunks)
  • packages/trpc/server/routers/viewer/organizations/update.schema.ts (2 hunks)
  • packages/trpc/server/routers/viewer/teams/update.handler.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.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/trpc/server/routers/viewer/teams/update.handler.ts
  • apps/web/playwright/lib/orgMigration.ts
  • packages/features/ee/billing/teams/internal-team-billing.ts
  • packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.ts
  • apps/api/v2/src/modules/teams/teams/teams.repository.ts
  • packages/trpc/server/routers/viewer/organizations/adminUpdate.handler.ts
  • packages/trpc/server/routers/viewer/organizations/publish.handler.ts
  • apps/web/app/api/teams/[team]/upgrade/route.ts
  • packages/trpc/server/routers/viewer/organizations/createTeams.handler.ts
  • packages/lib/server/repository/organization.ts
  • packages/trpc/server/routers/viewer/organizations/update.schema.ts
  • packages/trpc/server/routers/viewer/organizations/update.handler.ts
  • packages/prisma/zod-utils.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/trpc/server/routers/viewer/teams/update.handler.ts
  • apps/web/playwright/lib/orgMigration.ts
  • packages/features/ee/billing/teams/internal-team-billing.ts
  • packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.ts
  • apps/api/v2/src/modules/teams/teams/teams.repository.ts
  • packages/trpc/server/routers/viewer/organizations/adminUpdate.handler.ts
  • packages/trpc/server/routers/viewer/organizations/publish.handler.ts
  • apps/web/app/api/teams/[team]/upgrade/route.ts
  • packages/trpc/server/routers/viewer/organizations/createTeams.handler.ts
  • packages/lib/server/repository/organization.ts
  • packages/trpc/server/routers/viewer/organizations/update.schema.ts
  • packages/trpc/server/routers/viewer/organizations/update.handler.ts
  • packages/prisma/zod-utils.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/trpc/server/routers/viewer/teams/update.handler.ts
  • apps/web/playwright/lib/orgMigration.ts
  • packages/features/ee/billing/teams/internal-team-billing.ts
  • packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.ts
  • apps/api/v2/src/modules/teams/teams/teams.repository.ts
  • packages/trpc/server/routers/viewer/organizations/adminUpdate.handler.ts
  • packages/trpc/server/routers/viewer/organizations/publish.handler.ts
  • apps/web/app/api/teams/[team]/upgrade/route.ts
  • packages/trpc/server/routers/viewer/organizations/createTeams.handler.ts
  • packages/lib/server/repository/organization.ts
  • packages/trpc/server/routers/viewer/organizations/update.schema.ts
  • packages/trpc/server/routers/viewer/organizations/update.handler.ts
  • packages/prisma/zod-utils.ts
**/*.{service,repository}.ts

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

Avoid dot-suffixes like .service.ts or .repository.ts for new files; reserve .test.ts, .spec.ts, .types.ts for their specific purposes

Files:

  • apps/api/v2/src/modules/teams/teams/teams.repository.ts
🧬 Code graph analysis (13)
packages/trpc/server/routers/viewer/teams/update.handler.ts (1)
packages/prisma/zod-utils.ts (1)
  • teamMetadataStrictSchema (396-412)
apps/web/playwright/lib/orgMigration.ts (1)
packages/prisma/zod-utils.ts (1)
  • teamMetadataStrictSchema (396-412)
packages/features/ee/billing/teams/internal-team-billing.ts (1)
packages/prisma/zod-utils.ts (1)
  • teamMetadataStrictSchema (396-412)
packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.ts (1)
packages/prisma/zod-utils.ts (1)
  • teamMetadataStrictSchema (396-412)
apps/api/v2/src/modules/teams/teams/teams.repository.ts (1)
packages/prisma/zod-utils.ts (1)
  • teamMetadataStrictSchema (396-412)
packages/trpc/server/routers/viewer/organizations/adminUpdate.handler.ts (1)
packages/prisma/zod-utils.ts (1)
  • teamMetadataStrictSchema (396-412)
packages/trpc/server/routers/viewer/organizations/publish.handler.ts (1)
packages/prisma/zod-utils.ts (1)
  • teamMetadataStrictSchema (396-412)
apps/web/app/api/teams/[team]/upgrade/route.ts (2)
packages/prisma/zod-utils.ts (1)
  • teamMetadataStrictSchema (396-412)
packages/features/ee/billing/teams/internal-team-billing.ts (2)
  • team (29-32)
  • team (33-35)
packages/trpc/server/routers/viewer/organizations/createTeams.handler.ts (1)
packages/prisma/zod-utils.ts (1)
  • teamMetadataStrictSchema (396-412)
packages/lib/server/repository/organization.ts (1)
packages/prisma/zod-utils.ts (1)
  • teamMetadataStrictSchema (396-412)
packages/trpc/server/routers/viewer/organizations/update.schema.ts (1)
packages/prisma/zod-utils.ts (1)
  • teamMetadataStrictSchema (396-412)
packages/trpc/server/routers/viewer/organizations/update.handler.ts (1)
packages/prisma/zod-utils.ts (1)
  • teamMetadataStrictSchema (396-412)
packages/prisma/zod-utils.ts (1)
packages/platform/libraries/index.ts (1)
  • teamMetadataSchema (63-63)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Tests / Unit
  • GitHub Check: Type check / check-types
🔇 Additional comments (12)
apps/web/app/api/teams/[team]/upgrade/route.ts (1)

15-15: Good swap to strict schema import.

Switching to teamMetadataStrictSchema is aligned with the intent to constrain Stripe identifiers and keeps the route’s validation consistent with the rest of the PR.

packages/features/ee/billing/teams/internal-team-billing.ts (3)

13-13: Good: strict schema import.

Ensures this class only works with subscription ids that match Stripe’s prefixes.


20-21: unwrap usage is appropriate.

teamMetadataStrictSchema.unwrap() yields a non-nullable, partial object, which fits getMetadataHelpers’ merge use.


143-158: Runtime guarantees rely on strict schema; keep explicit guards.

updateQuantity correctly throws if subscriptionId or subscriptionItemId is missing. With the new schema optionality, these explicit checks remain essential. Looks good.

packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.ts (1)

33-33: Import switch to strict schema is correct and aligned with PR goals.

Using teamMetadataStrictSchema here matches the intent to enforce Stripe ID prefixes in org/team metadata.

packages/trpc/server/routers/viewer/teams/update.handler.ts (1)

11-11: Schema import change looks good.

Adopting teamMetadataStrictSchema here aligns with the Stripe ID prefix enforcement.

packages/trpc/server/routers/viewer/organizations/update.schema.ts (1)

4-4: Good move to the strict schema import.

This ensures update inputs are validated with the new Stripe ID prefix constraints.

packages/trpc/server/routers/viewer/organizations/createTeams.handler.ts (1)

298-307: LGTM: getSubscriptionId now enforces the “sub_” prefix.

Using teamMetadataStrictSchema.safeParse ensures we only attempt cancellation with valid Stripe subscription IDs, reducing API errors.

packages/trpc/server/routers/viewer/organizations/publish.handler.ts (1)

6-6: Strict schema import is aligned with the Stripe metadata restriction.

No concerns on the import itself.

packages/trpc/server/routers/viewer/organizations/update.handler.ts (2)

12-12: Importing strict schema is consistent with the PR goal

Switching to teamMetadataStrictSchema aligns this handler with the stricter Stripe metadata rules. No functional regressions here.


168-171: Sanity check: unwrap semantics verified
All viewer write routes now consistently use the strict schema’s unwrap() when merging metadata, preserving field validations without reintroducing the nullable wrapper. Confirmed usage in:

  • packages/trpc/server/routers/viewer/organizations/update.handler.ts
  • packages/trpc/server/routers/viewer/organizations/adminUpdate.handler.ts

No other getMetadataHelpers or mergeMetadata calls remain using the non-strict schema for writes.

packages/trpc/server/routers/viewer/organizations/adminUpdate.handler.ts (1)

7-7: LGTM: use strict schema in admin handler

Importing teamMetadataStrictSchema is consistent with the broader migration. No functional change here.

Copy link
Contributor

@emrysal emrysal left a comment

Choose a reason for hiding this comment

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

Looks good to me, happy to merge 😎

@emrysal emrysal merged commit 2ec68ef into calcom:main Aug 26, 2025
51 of 58 checks passed
@github-actions
Copy link
Contributor

E2E results are ready!

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 ready-for-e2e

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants