Skip to content

Conversation

sean-brydon
Copy link
Member

@sean-brydon sean-brydon commented Oct 9, 2025

Setup guide for configuring billing between selfhosted and billing enabled factory can be fouund in this readme

https://github.com/calcom/cal.com/blob/f3b9fb6a3b17ad3596752112aed0be42ee531f8a/packages/features/ee/organizations/lib/onboarding/OrganizationOnboardingSetup.md

Copy link
Member Author

Choose a reason for hiding this comment

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

Not removing as theyre sill used and will be picked up depricating in the v3 onboarding flow instead of just the backend changes

Comment on lines +93 to +94
brandColor: input.brandColor ?? null,
bannerUrl: input.bannerUrl ?? null,
Copy link
Member Author

Choose a reason for hiding this comment

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

added support for saving brandColor and bannerURL to the onboarding table for v3 flow

return organizationOnboarding;
}

protected filterTeamsAndInvites(teams: TeamInput[] = [], invitedMembers: InvitedMemberInput[] = []) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Rest of this comes from createOrganizationFromOnboarding.ts file that has been removed in this PR. No logic has changed

Copy link
Contributor

github-actions bot commented Oct 10, 2025

E2E results are ready!

@emrysal emrysal changed the title chore: organziation onboarding refactor chore: organization onboarding refactor Oct 10, 2025
@sean-brydon sean-brydon marked this pull request as ready for review October 13, 2025 07:54
@sean-brydon sean-brydon requested a review from a team as a code owner October 13, 2025 07:54
@graphite-app graphite-app bot requested a review from a team October 13, 2025 07:54
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: 7

🧹 Nitpick comments (30)
packages/features/ee/organizations/lib/utils.ts (2)

15-45: Use Set for O(1) lookup performance.

The current array requires O(n) lookups via includes(). For a list of 35+ providers called frequently during onboarding flows, a Set provides O(1) lookups and better performance.

Apply this diff to improve performance:

-  const personalEmailProviders = [
+  const personalEmailProviders = new Set([
     "gmail.com",
     "yahoo.com",
     "outlook.com",
     "hotmail.com",
     "aol.com",
     "icloud.com",
     "mail.com",
     "protonmail.com",
     "proton.me",
     "zoho.com",
     "yandex.com",
     "gmx.com",
     "fastmail.com",
     "inbox.com",
     "me.com",
     "hushmail.com",
     "live.com",
     "rediffmail.com",
     "tutanota.com",
     "mail.ru",
     "usa.com",
     "qq.com",
     "163.com",
     "web.de",
     "rocketmail.com",
     "excite.com",
     "lycos.com",
     "outlook.co",
     "hotmail.co.uk",
-  ];
+  ]);

   const emailParts = email.split("@");
   if (emailParts.length < 2) return false;
-  return !personalEmailProviders.includes(emailParts[1]);
+  return !personalEmailProviders.has(emailParts[1].toLowerCase());

47-49: Handle edge cases in isCompanyEmail

  • Trim and normalize the input to remove leading/trailing whitespace
  • Explicitly reject malformed emails containing multiple @
  • If subdomains of personal providers should be treated as personal (e.g., mail.google.com), match on domain suffix rather than exact domain
apps/web/playwright/organization/organization-creation.e2e.ts (1)

467-471: Gate on field visibility to avoid flag/UI divergence in E2E

Imported flag may not always mirror client-rendered state. Prefer filling only when inputs are actually visible to reduce flakiness.

Apply this diff:

-  // Fill in seat information only if billing is enabled
-  if (IS_TEAM_BILLING_ENABLED) {
-    await page.locator("input[name=seats]").fill("30");
-    await page.locator("input[name=pricePerSeat]").fill("30");
-  }
+  // Fill seats/pricing only if the inputs are rendered (billing-enabled flow)
+  if (IS_TEAM_BILLING_ENABLED) {
+    const seats = page.locator('input[name="seats"]');
+    if (await seats.isVisible()) {
+      await seats.fill("30");
+    }
+    const pricePerSeat = page.locator('input[name="pricePerSeat"]');
+    if (await pricePerSeat.isVisible()) {
+      await pricePerSeat.fill("30");
+    }
+  }
packages/prisma/schema.prisma (1)

2364-2365: Consider adding validation for branding fields.

The nullable fields are appropriate for optional branding. However, you may want to add validation at the application layer:

  • brandColor: Validate as hex color code (e.g., #RRGGBB)
  • bannerUrl: Validate as a URL format
packages/features/ee/organizations/types/schemas.ts (1)

10-11: Consider adding format validation for branding fields.

The .nullish() modifier is appropriate for optional fields. Consider adding format validation:

  • brandColor: Use .regex(/^#[0-9A-Fa-f]{6}$/) to ensure valid hex color codes
  • bannerUrl: Use .url() to ensure valid URL format

Example:

-  brandColor: z.string().nullish(),
-  bannerUrl: z.string().nullish(),
+  brandColor: z.string().regex(/^#[0-9A-Fa-f]{6}$/).nullish(),
+  bannerUrl: z.string().url().nullish(),
packages/features/ee/organizations/lib/server/orgCreationUtils.ts (1)

189-196: Feature‑flagged email verification: logic looks good; consider caching and fail‑closed behavior

  • OK: Gate on feature flag and always require completedOnboarding.
  • Suggest caching the global flag (e.g., short TTL) to avoid a DB hit per call.
  • If the feature lookup throws, consider defaulting to “require verification” to be safe, and log the failure.

Example refactor:

-  const featuresRepository = new FeaturesRepository(prisma);
-  const emailVerificationEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally(
-    "email-verification"
-  );
+  // hoist and cache at module scope (pseudo)
+  // const emailVerificationEnabled = await cachedFeatures.isEnabled("email-verification");
packages/features/ee/billing/api/webhook/_invoice.paid.org.ts (1)

95-115: Webhook user context uses ADMIN; consider a dedicated “system” role to avoid ambiguity

Using ADMIN in a system/webhook context works but can conflate privileges with real admins. Prefer a distinct system role in OnboardingUser (e.g., "SYSTEM") or pass a clearly named service account.

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

1-9: Deprecation note is clear

Nice guidance on migration; consider adding a removal target version/date to drive cleanup.

packages/features/ee/organizations/lib/onboarding/OrganizationOnboardingSetup.md (1)

29-41: Fix fenced code block language identifiers (markdownlint MD040)

  • Add a language to the “Decision Logic” block (e.g., text).
  • The trailing fenced block at Line 170 also needs a language or removal if unintended.

Example:

-```
+```text
 IF process.env.NEXT_PUBLIC_IS_E2E is set:
   → Use SelfHostedOnboardingService (E2E tests always skip billing)
 ...




Also applies to: 170-170

</blockquote></details>
<details>
<summary>packages/trpc/server/routers/viewer/organizations/createSelfHosted.handler.ts (1)</summary><blockquote>

`48-53`: **Avoid type assertion for role; normalize explicitly**

Cast to "ADMIN" | "USER" via a small mapper to prevent accidental widening if new roles appear.

Example:

```ts
const toOnboardingRole = (r: string): "ADMIN" | "USER" => (r === "ADMIN" ? "ADMIN" : "USER");
const userContext = { id: ctx.user.id, email: ctx.user.email, role: toOnboardingRole(ctx.user.role), name: ctx.user.name || undefined };
packages/features/ee/organizations/lib/OrganizationPermissionService.ts (1)

11-12: Switch to OnboardingUser: LGTM; align role checks

Now that role is "ADMIN" | "USER", prefer direct string checks instead of importing enum constants from other packages for clarity.

Example (outside this hunk):

hasPermissionToModifyDefaultPayment(): boolean {
  return this.user.role === "ADMIN";
}

Also applies to: 32-32

packages/features/ee/organizations/lib/OrganizationPaymentService.ts (1)

18-20: Nit: compare role via string literal to avoid cross‑enum coupling

Since user.role is "ADMIN" | "USER", prefer this.user.role === "ADMIN" over importing UserPermissionRole enums for comparisons elsewhere in this class (e.g., admin shortcut in createPaymentIntent).

packages/features/ee/organizations/lib/onboarding/__tests__/OrganizationOnboardingFactory.test.ts (1)

118-131: Remove duplicate test case.

This test case at lines 118-131 is identical to the test at lines 38-51, both verifying that a regular user gets BillingEnabledOnboardingService when billing is enabled.

Apply this diff to remove the duplicate:

-    it("should return BillingEnabledOnboardingService when billing is enabled for regular user", async () => {
-      vi.doMock("@calcom/lib/constants", async (importOriginal) => {
-        const actual = await importOriginal<typeof import("@calcom/lib/constants")>();
-        return {
-          ...actual,
-          IS_TEAM_BILLING_ENABLED: true,
-        };
-      });
-
-      const { OrganizationOnboardingFactory: Factory } = await import("../OrganizationOnboardingFactory");
-      const service = Factory.create(mockRegularUser as any);
-
-      expect(service.constructor.name).toBe("BillingEnabledOnboardingService");
-    });
packages/trpc/server/routers/viewer/organizations/createWithPaymentIntent.schema.ts (1)

15-18: Deprecate/remove local BillingPeriod and align on a single source.

This file still exports a local BillingPeriod enum while the app uses @calcom/prisma/enums elsewhere. To avoid drift, either:

  • import and re-export the shared enum here, or
  • mark this local enum as deprecated and remove usages.

Also, consider marking the type alias as deprecated to propagate IDE warnings.

Apply these diffs:

-/**
- * @deprecated Use ZIntentToCreateOrgInputSchema instead
- */
+/**
+ * @deprecated Use ZIntentToCreateOrgInputSchema instead
+ */
 export const ZCreateWithPaymentIntentInputSchema = createOrganizationSchema;
 export type TCreateWithPaymentIntentInputSchema = z.infer<typeof ZCreateWithPaymentIntentInputSchema>;
+
+/** @deprecated Use TIntentToCreateOrgInputSchema instead */
+export type TCreateWithPaymentIntentInputSchema = z.infer<typeof ZCreateWithPaymentIntentInputSchema>;
-export enum BillingPeriod {
-  MONTHLY = "MONTHLY",
-  ANNUALLY = "ANNUALLY",
-}
+// @deprecated Prefer BillingPeriod from @calcom/prisma/enums
+export enum BillingPeriod {
+  MONTHLY = "MONTHLY",
+  ANNUALLY = "ANNUALLY",
+}

Also applies to: 10-13

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

31-34: Avoid logging raw emails (PII).

Drop orgOwnerEmail from debug logs or mask it to reduce PII in logs.

-  log.debug(
-    "Starting organization creation intent",
-    safeStringify({ slug, name, orgOwnerEmail, isPlatform })
-  );
+  log.debug(
+    "Starting organization creation intent",
+    safeStringify({ slug, name, isPlatform })
+  );

101-101: Prefer named exports; remove the default export
All callers already import intentToCreateOrgHandler by name, so you can safely delete the final export default intentToCreateOrgHandler;.

packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx (2)

138-159: Localize UI strings with t().

Replace hardcoded strings to comply with i18n guidelines.

As per coding guidelines

-<Label htmlFor="billingPeriod">Billing Period</Label>
+<Label htmlFor="billingPeriod">{t("billing_period")}</Label>
 ...
-  options={[
-    { value: "MONTHLY", label: "Monthly" },
-    { value: "ANNUALLY", label: "Annually" },
-  ]}
+  options={[
+    { value: "MONTHLY", label: t("monthly") },
+    { value: "ANNUALLY", label: t("annually") },
+  ]}
- label="Seats (optional)"
+ label={t("seats_optional")}
- label="Price per seat (optional)"
+ label={t("price_per_seat_optional")}
- <h3 className="font-cal text-default text-lg font-semibold leading-4">
-   Upgrade to Organizations
- </h3>
+ <h3 className="font-cal text-default text-lg font-semibold leading-4">
+   {t("upgrade_to_organizations")}
+ </h3>

Also applies to: 263-299, 306-308


286-291: Handle empty input safely when parsing numbers.

Avoid NaN when users clear the field.

-onChange={(e) => {
-  onChange(+e.target.value);
-}}
+onChange={(e) => {
+  const v = e.target.value;
+  onChange(v === "" ? null : +v);
+}}
packages/features/ee/organizations/lib/onboarding/SelfHostedOnboardingService.ts (1)

101-106: Return the persisted billingPeriod.

The intent response should reflect what’s stored (incl. defaults), not the raw input.

-      billingPeriod: input.billingPeriod,
+      billingPeriod: input.billingPeriod ?? organizationOnboarding.billingPeriod,
packages/features/ee/organizations/lib/onboarding/BillingEnabledOnboardingService.ts (2)

93-93: Return the persisted billingPeriod.

Prefer the stored value (with default) over input.

-      billingPeriod: input.billingPeriod,
+      billingPeriod: input.billingPeriod ?? organizationOnboarding.billingPeriod,

136-151: Use onboarding isPlatform value.

Avoid forcing isPlatform to false when creating the organization.

-      isPlatform: false,
+      isPlatform: organizationOnboarding.isPlatform,
packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.schema.ts (1)

18-22: Consider adding format validation for brand fields.

The brand fields (logo, bannerUrl, brandColor) currently accept any string. Consider adding validation:

  • logo and bannerUrl: validate URL format with .url() or custom regex
  • brandColor: validate hex color format with regex (e.g., /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/)

This would improve data quality and catch errors earlier in the validation pipeline.

Example:

  // Brand fields
- logo: z.string().nullish(),
+ logo: z.string().url().nullish(),
  bio: z.string().nullish(),
- brandColor: z.string().nullish(),
+ brandColor: z.string().regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/).nullish(),
- bannerUrl: z.string().nullish(),
+ bannerUrl: z.string().url().nullish(),
packages/features/ee/organizations/lib/onboarding/__tests__/SelfHostedOnboardingService.test.ts (1)

99-111: Use enum constant instead of string literal.

Line 107 uses the string "MONTHLY" instead of the BillingPeriod.MONTHLY enum constant. While this works due to the enum value, using the constant is more type-safe and maintainable.

Apply this diff:

      createOrganizationOnboarding: vi.fn().mockResolvedValue({
        id: "onboarding-123",
        name: mockInput.name,
        slug: mockInput.slug,
        orgOwnerEmail: mockInput.orgOwnerEmail,
        seats: mockInput.seats,
        pricePerSeat: mockInput.pricePerSeat,
-       billingPeriod: "MONTHLY",
+       billingPeriod: BillingPeriod.MONTHLY,
        isComplete: false,
        stripeCustomerId: null,
      }),
packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts (1)

29-29: Use enum constant instead of string literal.

The creationSource field uses the string literal "WEBAPP" instead of the enum constant CreationSource.WEBAPP. While this works due to TypeScript's structural typing, using the constant is more type-safe.

Apply this diff:

  const mockInput = {
    name: "Test Org",
    slug: "test-org",
    orgOwnerEmail: "[email protected]",
    billingPeriod: "MONTHLY",
    seats: 5,
    pricePerSeat: 20,
    isPlatform: false,
-   creationSource: "WEBAPP",
+   creationSource: CreationSource.WEBAPP,
  };
packages/features/ee/organizations/lib/onboarding/BaseOnboardingService.ts (4)

72-100: Persist teams/invitedMembers when creating onboarding record

Input supports teams/invitedMembers, but they aren’t saved. Persist or confirm they’re set later; otherwise downstream flows won’t see them.

Example:

   protected async createOnboardingRecord(input: CreateOnboardingIntentInput) {
+    const { teamsData, invitedMembersData } = this.filterTeamsAndInvites(
+      input.teams ?? [],
+      input.invitedMembers ?? []
+    );
     const organizationOnboarding = await this.paymentService.createOrganizationOnboarding({
       name: input.name,
       slug: input.slug,
       orgOwnerEmail: input.orgOwnerEmail,
       seats: input.seats,
       pricePerSeat: input.pricePerSeat,
       billingPeriod: input.billingPeriod,
       createdByUserId: this.user.id,
       logo: input.logo ?? null,
       bio: input.bio ?? null,
       brandColor: input.brandColor ?? null,
       bannerUrl: input.bannerUrl ?? null,
+      teams: teamsData,
+      invitedMembers: invitedMembersData,
     });

If OrganizationPaymentService.createOrganizationOnboarding doesn’t accept these fields, consider updating it accordingly.


102-123: Validate and normalize with zod; constrain roles

Use the provided zod schemas and normalize role/email to prevent invalid invites.

-  protected filterTeamsAndInvites(teams: TeamInput[] = [], invitedMembers: InvitedMemberInput[] = []) {
-    const teamsData = teams
+  protected filterTeamsAndInvites(teams: TeamInput[] = [], invitedMembers: InvitedMemberInput[] = []) {
+    const teamsDataRaw = teams
       .filter((team) => team.name.trim().length > 0)
       .map((team) => ({
         id: team.id === -1 ? -1 : team.id,
         name: team.name,
         isBeingMigrated: team.isBeingMigrated,
         slug: team.slug,
       }));
 
-    const invitedMembersData = invitedMembers
+    const invitedMembersDataRaw = invitedMembers
       .filter((invite) => invite.email.trim().length > 0)
       .map((invite) => ({
-        email: invite.email,
+        email: invite.email.toLowerCase(),
         name: invite.name,
         teamId: invite.teamId,
         teamName: invite.teamName,
-        role: invite.role,
+        // Only ADMIN or MEMBER allowed; default to MEMBER
+        role: invite.role?.toUpperCase() === "ADMIN" ? "ADMIN" : "MEMBER",
       }));
 
-    return { teamsData, invitedMembersData };
+    const teamsData = teamsSchema.parse(teamsDataRaw);
+    const invitedMembersData = invitedMembersSchema.parse(invitedMembersDataRaw);
+    return { teamsData, invitedMembersData };
   }

190-199: Guard against duplicate default availability creation

If retried, this can create duplicate availabilities. Check existence before insert.

const existing = await prisma.availability.count({ where: { userId: owner.id } });
if (!existing) {
  await prisma.availability.createMany({ data: ... });
}

516-519: Mark domain configured only on success or clarify semantics

isDomainConfigured is set to true regardless of actual configuration outcome. Either:

  • set to true only if domain creation succeeded, or
  • rename/repurpose field to reflect “attempted” state.
packages/features/ee/organizations/lib/onboarding/types.ts (2)

11-17: Constrain role to allowed values

Align with zod schema and server expectations; avoid free-form strings.

-import type { BillingPeriod, CreationSource } from "@calcom/prisma/enums";
+import type { BillingPeriod, CreationSource, MembershipRole } from "@calcom/prisma/enums";
...
-export type InvitedMemberInput = {
+export type InvitedMemberInput = {
   email: string;
   name?: string;
   teamId?: number;
   teamName?: string;
-  role?: string;
+  role?: Extract<MembershipRole, "ADMIN" | "MEMBER">;
 };
...
-export type InvitedMember = {
+export type InvitedMember = {
   email: string;
   name?: string;
   teamId?: number;
   teamName?: string;
-  role?: string;
+  role?: Extract<MembershipRole, "ADMIN" | "MEMBER">;
 };

Also applies to: 101-107


77-92: Unify billingPeriod typing

Use the enum type consistently instead of a string union.

 export type OrganizationData = {
   id: number | null;
   name: string;
   slug: string;
   isOrganizationConfigured: boolean;
   isOrganizationAdminReviewed: boolean;
   autoAcceptEmail: string;
   seats: number | null;
   pricePerSeat: number | null;
   isPlatform: boolean;
   logoUrl: string | null;
   bio: string | null;
   brandColor: string | null;
   bannerUrl: string | null;
-  billingPeriod?: "MONTHLY" | "ANNUALLY";
+  billingPeriod?: BillingPeriod;
 };
📜 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 eae2711 and 9c72276.

📒 Files selected for processing (37)
  • apps/web/playwright/organization/organization-creation.e2e.ts (1 hunks)
  • packages/features/ee/billing/api/webhook/_invoice.paid.org.ts (2 hunks)
  • packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx (2 hunks)
  • packages/features/ee/organizations/lib/OrganizationPaymentService.test.ts (4 hunks)
  • packages/features/ee/organizations/lib/OrganizationPaymentService.ts (6 hunks)
  • packages/features/ee/organizations/lib/OrganizationPermissionService.ts (2 hunks)
  • packages/features/ee/organizations/lib/onboarding/BaseOnboardingService.ts (1 hunks)
  • packages/features/ee/organizations/lib/onboarding/BillingEnabledOnboardingService.ts (1 hunks)
  • packages/features/ee/organizations/lib/onboarding/IOrganizationOnboardingService.ts (1 hunks)
  • packages/features/ee/organizations/lib/onboarding/OrganizationOnboardingFactory.ts (1 hunks)
  • packages/features/ee/organizations/lib/onboarding/OrganizationOnboardingSetup.md (1 hunks)
  • packages/features/ee/organizations/lib/onboarding/SelfHostedOnboardingService.ts (1 hunks)
  • packages/features/ee/organizations/lib/onboarding/__tests__/BaseOnboardingService.test.ts (1 hunks)
  • packages/features/ee/organizations/lib/onboarding/__tests__/BillingEnabledOnboardingService.test.ts (1 hunks)
  • packages/features/ee/organizations/lib/onboarding/__tests__/OrganizationOnboardingFactory.test.ts (1 hunks)
  • packages/features/ee/organizations/lib/onboarding/__tests__/SelfHostedOnboardingService.test.ts (1 hunks)
  • packages/features/ee/organizations/lib/onboarding/index.ts (1 hunks)
  • packages/features/ee/organizations/lib/onboarding/types.ts (1 hunks)
  • packages/features/ee/organizations/lib/onboardingStore.ts (2 hunks)
  • packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.test.ts (0 hunks)
  • packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.ts (0 hunks)
  • packages/features/ee/organizations/lib/server/orgCreationUtils.ts (2 hunks)
  • packages/features/ee/organizations/lib/utils.ts (1 hunks)
  • packages/features/ee/organizations/types/schemas.ts (1 hunks)
  • packages/features/flags/config.ts (1 hunks)
  • packages/features/flags/hooks/index.ts (1 hunks)
  • packages/lib/server/repository/organizationOnboarding.ts (2 hunks)
  • packages/prisma/migrations/20251005102651_add_onboarding_v3_feature_flag/migration.sql (1 hunks)
  • packages/prisma/migrations/20251007090722_modify_onboarding_table_orgs/migration.sql (1 hunks)
  • packages/prisma/schema.prisma (1 hunks)
  • packages/prisma/zod-utils.ts (1 hunks)
  • packages/trpc/server/routers/viewer/organizations/createSelfHosted.handler.ts (2 hunks)
  • packages/trpc/server/routers/viewer/organizations/createWithPaymentIntent.handler.ts (3 hunks)
  • packages/trpc/server/routers/viewer/organizations/createWithPaymentIntent.schema.ts (2 hunks)
  • packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts (10 hunks)
  • packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.ts (4 hunks)
  • packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.schema.ts (2 hunks)
💤 Files with no reviewable changes (2)
  • packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.test.ts
  • packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.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:

  • apps/web/playwright/organization/organization-creation.e2e.ts
  • packages/features/ee/organizations/types/schemas.ts
  • packages/features/ee/organizations/lib/onboarding/index.ts
  • packages/features/ee/organizations/lib/onboarding/OrganizationOnboardingFactory.ts
  • packages/trpc/server/routers/viewer/organizations/createSelfHosted.handler.ts
  • packages/features/ee/organizations/lib/onboarding/SelfHostedOnboardingService.ts
  • packages/features/flags/config.ts
  • packages/features/ee/organizations/lib/utils.ts
  • packages/features/ee/organizations/lib/onboarding/BillingEnabledOnboardingService.ts
  • packages/features/ee/organizations/lib/onboarding/__tests__/BaseOnboardingService.test.ts
  • packages/features/ee/organizations/lib/onboarding/IOrganizationOnboardingService.ts
  • packages/features/ee/billing/api/webhook/_invoice.paid.org.ts
  • packages/features/flags/hooks/index.ts
  • packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.ts
  • packages/features/ee/organizations/lib/onboarding/__tests__/SelfHostedOnboardingService.test.ts
  • packages/trpc/server/routers/viewer/organizations/createWithPaymentIntent.handler.ts
  • packages/trpc/server/routers/viewer/organizations/createWithPaymentIntent.schema.ts
  • packages/lib/server/repository/organizationOnboarding.ts
  • packages/features/ee/organizations/lib/OrganizationPaymentService.test.ts
  • packages/features/ee/organizations/lib/server/orgCreationUtils.ts
  • packages/features/ee/organizations/lib/onboarding/__tests__/OrganizationOnboardingFactory.test.ts
  • packages/features/ee/organizations/lib/onboarding/BaseOnboardingService.ts
  • packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.schema.ts
  • packages/prisma/zod-utils.ts
  • packages/features/ee/organizations/lib/onboarding/__tests__/BillingEnabledOnboardingService.test.ts
  • packages/features/ee/organizations/lib/OrganizationPermissionService.ts
  • packages/features/ee/organizations/lib/onboardingStore.ts
  • packages/features/ee/organizations/lib/onboarding/types.ts
  • packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts
  • packages/features/ee/organizations/lib/OrganizationPaymentService.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:

  • apps/web/playwright/organization/organization-creation.e2e.ts
  • packages/features/ee/organizations/types/schemas.ts
  • packages/features/ee/organizations/lib/onboarding/index.ts
  • packages/features/ee/organizations/lib/onboarding/OrganizationOnboardingFactory.ts
  • packages/trpc/server/routers/viewer/organizations/createSelfHosted.handler.ts
  • packages/features/ee/organizations/lib/onboarding/SelfHostedOnboardingService.ts
  • packages/features/flags/config.ts
  • packages/features/ee/organizations/lib/utils.ts
  • packages/features/ee/organizations/lib/onboarding/BillingEnabledOnboardingService.ts
  • packages/features/ee/organizations/lib/onboarding/__tests__/BaseOnboardingService.test.ts
  • packages/features/ee/organizations/lib/onboarding/IOrganizationOnboardingService.ts
  • packages/features/ee/billing/api/webhook/_invoice.paid.org.ts
  • packages/features/flags/hooks/index.ts
  • packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.ts
  • packages/features/ee/organizations/lib/onboarding/__tests__/SelfHostedOnboardingService.test.ts
  • packages/trpc/server/routers/viewer/organizations/createWithPaymentIntent.handler.ts
  • packages/trpc/server/routers/viewer/organizations/createWithPaymentIntent.schema.ts
  • packages/lib/server/repository/organizationOnboarding.ts
  • packages/features/ee/organizations/lib/OrganizationPaymentService.test.ts
  • packages/features/ee/organizations/lib/server/orgCreationUtils.ts
  • packages/features/ee/organizations/lib/onboarding/__tests__/OrganizationOnboardingFactory.test.ts
  • packages/features/ee/organizations/lib/onboarding/BaseOnboardingService.ts
  • packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.schema.ts
  • packages/prisma/zod-utils.ts
  • packages/features/ee/organizations/lib/onboarding/__tests__/BillingEnabledOnboardingService.test.ts
  • packages/features/ee/organizations/lib/OrganizationPermissionService.ts
  • packages/features/ee/organizations/lib/onboardingStore.ts
  • packages/features/ee/organizations/lib/onboarding/types.ts
  • packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts
  • packages/features/ee/organizations/lib/OrganizationPaymentService.ts
  • packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx
**/*.{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:

  • apps/web/playwright/organization/organization-creation.e2e.ts
  • packages/features/ee/organizations/types/schemas.ts
  • packages/features/ee/organizations/lib/onboarding/index.ts
  • packages/features/ee/organizations/lib/onboarding/OrganizationOnboardingFactory.ts
  • packages/trpc/server/routers/viewer/organizations/createSelfHosted.handler.ts
  • packages/features/ee/organizations/lib/onboarding/SelfHostedOnboardingService.ts
  • packages/features/flags/config.ts
  • packages/features/ee/organizations/lib/utils.ts
  • packages/features/ee/organizations/lib/onboarding/BillingEnabledOnboardingService.ts
  • packages/features/ee/organizations/lib/onboarding/__tests__/BaseOnboardingService.test.ts
  • packages/features/ee/organizations/lib/onboarding/IOrganizationOnboardingService.ts
  • packages/features/ee/billing/api/webhook/_invoice.paid.org.ts
  • packages/features/flags/hooks/index.ts
  • packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.ts
  • packages/features/ee/organizations/lib/onboarding/__tests__/SelfHostedOnboardingService.test.ts
  • packages/trpc/server/routers/viewer/organizations/createWithPaymentIntent.handler.ts
  • packages/trpc/server/routers/viewer/organizations/createWithPaymentIntent.schema.ts
  • packages/lib/server/repository/organizationOnboarding.ts
  • packages/features/ee/organizations/lib/OrganizationPaymentService.test.ts
  • packages/features/ee/organizations/lib/server/orgCreationUtils.ts
  • packages/features/ee/organizations/lib/onboarding/__tests__/OrganizationOnboardingFactory.test.ts
  • packages/features/ee/organizations/lib/onboarding/BaseOnboardingService.ts
  • packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.schema.ts
  • packages/prisma/zod-utils.ts
  • packages/features/ee/organizations/lib/onboarding/__tests__/BillingEnabledOnboardingService.test.ts
  • packages/features/ee/organizations/lib/OrganizationPermissionService.ts
  • packages/features/ee/organizations/lib/onboardingStore.ts
  • packages/features/ee/organizations/lib/onboarding/types.ts
  • packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts
  • packages/features/ee/organizations/lib/OrganizationPaymentService.ts
  • packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx
**/*Service.ts

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

Service files must include Service suffix, use PascalCase matching exported class, and avoid generic names (e.g., MembershipService.ts)

Files:

  • packages/features/ee/organizations/lib/onboarding/SelfHostedOnboardingService.ts
  • packages/features/ee/organizations/lib/onboarding/BillingEnabledOnboardingService.ts
  • packages/features/ee/organizations/lib/onboarding/IOrganizationOnboardingService.ts
  • packages/features/ee/organizations/lib/onboarding/BaseOnboardingService.ts
  • packages/features/ee/organizations/lib/OrganizationPermissionService.ts
  • packages/features/ee/organizations/lib/OrganizationPaymentService.ts
**/*.tsx

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

Always use t() for text localization in frontend code; direct text embedding should trigger a warning

Files:

  • packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx
🧠 Learnings (1)
📚 Learning: 2025-08-27T13:32:46.887Z
Learnt from: supalarry
PR: calcom/cal.com#23364
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/internal-to-api.spec.ts:295-296
Timestamp: 2025-08-27T13:32:46.887Z
Learning: In calcom/cal.com, when transforming booking fields from internal to API format, tests in organizations-event-types.e2e-spec.ts already expect name field label and placeholder to be empty strings ("") rather than undefined. PR changes that set these to explicit empty strings are typically fixing implementation to match existing test expectations rather than breaking changes.

Applied to files:

  • packages/features/ee/organizations/lib/OrganizationPaymentService.test.ts
🧬 Code graph analysis (19)
apps/web/playwright/organization/organization-creation.e2e.ts (1)
packages/lib/constants.ts (1)
  • IS_TEAM_BILLING_ENABLED (118-118)
packages/features/ee/organizations/lib/onboarding/OrganizationOnboardingFactory.ts (3)
packages/features/ee/organizations/lib/onboarding/types.ts (1)
  • OnboardingUser (50-55)
packages/features/ee/organizations/lib/onboarding/IOrganizationOnboardingService.ts (1)
  • IOrganizationOnboardingService (13-32)
packages/lib/constants.ts (1)
  • IS_TEAM_BILLING_ENABLED (118-118)
packages/trpc/server/routers/viewer/organizations/createSelfHosted.handler.ts (1)
packages/features/ee/organizations/lib/onboarding/SelfHostedOnboardingService.ts (1)
  • SelfHostedOnboardingService (38-226)
packages/features/ee/organizations/lib/onboarding/SelfHostedOnboardingService.ts (5)
packages/prisma/zod-utils.ts (2)
  • orgOnboardingInvitedMembersSchema (60-68)
  • orgOnboardingTeamsSchema (70-78)
packages/features/ee/organizations/lib/onboarding/types.ts (4)
  • CreateOnboardingIntentInput (19-34)
  • OnboardingIntentResult (36-48)
  • OrganizationOnboardingData (57-75)
  • OrganizationData (77-92)
packages/lib/constants.ts (1)
  • IS_SELF_HOSTED (60-60)
packages/features/ee/common/server/LicenseKeyService.ts (1)
  • LicenseKeySingleton (137-149)
packages/features/ee/organizations/lib/server/orgCreationUtils.ts (1)
  • findUserToBeOrgOwner (264-292)
packages/features/ee/organizations/lib/onboarding/BillingEnabledOnboardingService.ts (6)
packages/prisma/zod-utils.ts (2)
  • orgOnboardingInvitedMembersSchema (60-68)
  • orgOnboardingTeamsSchema (70-78)
packages/features/ee/organizations/lib/onboarding/types.ts (4)
  • CreateOnboardingIntentInput (19-34)
  • OnboardingIntentResult (36-48)
  • OrganizationOnboardingData (57-75)
  • OrganizationData (77-92)
packages/lib/server/repository/organizationOnboarding.ts (1)
  • OrganizationOnboardingRepository (31-143)
packages/lib/constants.ts (1)
  • IS_SELF_HOSTED (60-60)
packages/features/ee/organizations/lib/server/orgCreationUtils.ts (1)
  • findUserToBeOrgOwner (264-292)
packages/lib/server/repository/organization.ts (1)
  • OrganizationRepository (23-479)
packages/features/ee/organizations/lib/onboarding/IOrganizationOnboardingService.ts (1)
packages/features/ee/organizations/lib/onboarding/types.ts (3)
  • CreateOnboardingIntentInput (19-34)
  • OnboardingIntentResult (36-48)
  • OrganizationOnboardingData (57-75)
packages/features/ee/billing/api/webhook/_invoice.paid.org.ts (1)
packages/features/ee/organizations/lib/onboarding/BillingEnabledOnboardingService.ts (1)
  • BillingEnabledOnboardingService (37-221)
packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.ts (2)
packages/lib/server/repository/organizationOnboarding.ts (1)
  • OrganizationOnboardingRepository (31-143)
packages/features/ee/organizations/lib/onboarding/OrganizationOnboardingFactory.ts (1)
  • OrganizationOnboardingFactory (19-56)
packages/features/ee/organizations/lib/onboarding/__tests__/SelfHostedOnboardingService.test.ts (6)
packages/features/ee/organizations/lib/onboarding/types.ts (2)
  • CreateOnboardingIntentInput (19-34)
  • OrganizationOnboardingData (57-75)
packages/features/ee/organizations/lib/onboarding/SelfHostedOnboardingService.ts (1)
  • SelfHostedOnboardingService (38-226)
packages/features/ee/common/server/LicenseKeyService.ts (1)
  • LicenseKeySingleton (137-149)
packages/lib/server/repository/organizationOnboarding.ts (1)
  • OrganizationOnboardingRepository (31-143)
packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts (1)
  • inviteMembersWithNoInviterPermissionCheck (134-242)
packages/trpc/server/routers/viewer/organizations/createTeams.handler.ts (1)
  • createTeamsHandler (30-131)
packages/trpc/server/routers/viewer/organizations/createWithPaymentIntent.handler.ts (1)
packages/features/ee/organizations/lib/OrganizationPaymentService.ts (1)
  • OrganizationPaymentService (78-395)
packages/features/ee/organizations/lib/onboarding/__tests__/OrganizationOnboardingFactory.test.ts (1)
packages/features/ee/organizations/lib/onboarding/OrganizationOnboardingFactory.ts (1)
  • isBillingEnabled (41-55)
packages/features/ee/organizations/lib/onboarding/BaseOnboardingService.ts (6)
packages/prisma/zod-utils.ts (4)
  • orgOnboardingInvitedMembersSchema (60-68)
  • orgOnboardingTeamsSchema (70-78)
  • userMetadata (332-354)
  • teamMetadataStrictSchema (395-411)
packages/features/ee/organizations/lib/server/orgCreationUtils.ts (3)
  • findUserToBeOrgOwner (264-292)
  • assertCanCreateOrg (176-262)
  • setupDomain (294-347)
packages/features/ee/organizations/lib/onboarding/IOrganizationOnboardingService.ts (1)
  • IOrganizationOnboardingService (13-32)
packages/features/ee/organizations/lib/onboarding/types.ts (8)
  • OnboardingUser (50-55)
  • CreateOnboardingIntentInput (19-34)
  • OrganizationOnboardingData (57-75)
  • TeamInput (4-9)
  • InvitedMemberInput (11-17)
  • OrganizationData (77-92)
  • TeamData (94-99)
  • InvitedMember (101-107)
packages/features/ee/organizations/lib/OrganizationPaymentService.ts (1)
  • OrganizationPaymentService (78-395)
packages/lib/server/repository/organizationOnboarding.ts (1)
  • OrganizationOnboardingRepository (31-143)
packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.schema.ts (1)
packages/prisma/zod-utils.ts (2)
  • orgOnboardingTeamsSchema (70-78)
  • orgOnboardingInvitedMembersSchema (60-68)
packages/features/ee/organizations/lib/onboarding/__tests__/BillingEnabledOnboardingService.test.ts (4)
packages/features/ee/organizations/lib/onboarding/types.ts (1)
  • CreateOnboardingIntentInput (19-34)
packages/features/ee/organizations/lib/onboarding/BillingEnabledOnboardingService.ts (1)
  • BillingEnabledOnboardingService (37-221)
packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts (1)
  • inviteMembersWithNoInviterPermissionCheck (134-242)
packages/trpc/server/routers/viewer/organizations/createTeams.handler.ts (1)
  • createTeamsHandler (30-131)
packages/features/ee/organizations/lib/OrganizationPermissionService.ts (1)
packages/features/ee/organizations/lib/onboarding/types.ts (1)
  • OnboardingUser (50-55)
packages/features/ee/organizations/lib/onboardingStore.ts (2)
packages/features/ee/organizations/lib/onboarding/OrganizationOnboardingFactory.ts (1)
  • isBillingEnabled (41-55)
packages/lib/constants.ts (1)
  • IS_TEAM_BILLING_ENABLED_CLIENT (120-121)
packages/features/ee/organizations/lib/onboarding/types.ts (2)
packages/features/ee/organizations/lib/onboarding/index.ts (2)
  • CreateOnboardingIntentInput (3-3)
  • OnboardingIntentResult (3-3)
packages/platform/libraries/index.ts (1)
  • CreationSource (28-28)
packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts (3)
packages/features/ee/organizations/lib/OrganizationPaymentService.ts (1)
  • OrganizationPaymentService (78-395)
packages/features/ee/common/server/LicenseKeyService.ts (1)
  • LicenseKeySingleton (137-149)
packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.ts (1)
  • intentToCreateOrgHandler (29-99)
packages/features/ee/organizations/lib/OrganizationPaymentService.ts (2)
packages/features/ee/organizations/lib/onboarding/types.ts (1)
  • OnboardingUser (50-55)
packages/features/ee/organizations/lib/OrganizationPermissionService.ts (1)
  • OrganizationPermissionService (31-141)
🪛 markdownlint-cli2 (0.18.1)
packages/features/ee/organizations/lib/onboarding/OrganizationOnboardingSetup.md

29-29: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


170-170: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (52)
packages/prisma/migrations/20251005102651_add_onboarding_v3_feature_flag/migration.sql (1)

1-9: LGTM!

The migration correctly adds the onboarding-v3 feature flag with appropriate safeguards:

  • Uses ON CONFLICT DO NOTHING to prevent errors on re-runs
  • Defaults to enabled = false for safe rollout
  • Type EXPERIMENT is appropriate for a v3 feature
packages/prisma/zod-utils.ts (1)

60-68: LGTM!

The schema extensions for invited members are well-designed:

  • New fields (teamId, teamName, role) are appropriately optional
  • Role default of "MEMBER" is sensible for new invites
  • Enum values align with team membership roles
packages/prisma/migrations/20251007090722_modify_onboarding_table_orgs/migration.sql (1)

1-3: LGTM!

The migration correctly adds the branding fields to the OrganizationOnboarding table:

  • Both columns are nullable (safe for existing rows)
  • Consistent with the Prisma schema changes
packages/features/flags/hooks/index.ts (1)

30-30: LGTM!

The feature flag is properly initialized with a safe default value of false.

packages/features/ee/organizations/lib/OrganizationPaymentService.test.ts (3)

115-158: LGTM!

The test correctly asserts the new branding fields (brandColor, bannerUrl) are set to null when not provided in the input, which is consistent with the nullable schema design.


160-208: LGTM!

The test correctly validates seat calculation with the new branding fields included in the expected data shape.


219-255: LGTM!

The admin override tests correctly assert that invitedMembers is undefined when not provided in the input, which is the expected behavior.

packages/features/flags/config.ts (1)

31-31: LGTM!

The new flag is correctly added to the AppFlags type definition, and will be automatically propagated to TeamFeatures through keyof AppFlags.

packages/features/ee/billing/api/webhook/_invoice.paid.org.ts (1)

116-120: Guard against missing subscription item id

createOrganization will throw if payment details are missing. Add an early check to avoid noisy errors when invoice.lines[0]?.subscription_item is absent.

Apply:

-const { organization } = await onboardingService.createOrganization(organizationOnboarding, {
+if (!paymentSubscriptionItemId) {
+  logger.warn("Missing subscription item id; skipping org creation", safeStringify({ onboardingId: organizationOnboarding.id }));
+  return { success: true };
+}
+const { organization } = await onboardingService.createOrganization(organizationOnboarding, {
   subscriptionId: paymentSubscriptionId,
   subscriptionItemId: paymentSubscriptionItemId,
 });
packages/trpc/server/routers/viewer/organizations/createWithPaymentIntent.handler.ts (2)

32-40: Constructing OnboardingUser: LGTM

Typed context aligns with OrganizationPaymentService changes.


81-83: Forwarding branding fields: LGTM

brandColor and bannerUrl are propagated correctly.

Ensure downstream zod/schema and DB migrations include these fields (they appear in OrganizationOnboardingRepository and services).

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

55-74: Self‑hosted onboarding via service: LGTM

Payload shape matches SelfHostedOnboardingService expectations; completion marking retained.

packages/lib/server/repository/organizationOnboarding.ts (2)

19-20: Type surface extended with branding fields: LGTM


46-47: Persisting brandColor/bannerUrl: LGTM

Ensure the migration added these columns and they are nullable.

packages/features/ee/organizations/lib/OrganizationPaymentService.ts (5)

24-30: Input surfaces extended (branding and invitedMembers fields): LGTM

Also applies to: 32-44


81-87: Constructor now uses OnboardingUser: LGTM

Keeps permission service aligned with new user context.


200-204: Persist branding fields on onboarding create: LGTM


300-300: Destructuring includes branding fields: LGTM


377-382: Persist branding fields on onboarding update: LGTM

Confirm DB columns and zod schemas match (migrations present in this PR).

packages/features/ee/organizations/lib/onboarding/OrganizationOnboardingFactory.ts (3)

1-11: LGTM!

The imports are well-organized and the logger setup follows the standard pattern with a clear prefix for this factory.


13-39: LGTM!

The factory pattern is well-implemented with clear documentation explaining the billing logic. The comprehensive logging with safeStringify will help with debugging onboarding flow selection.


41-55: LGTM!

The billing eligibility logic is correct and well-documented. The three-tier decision (E2E → self-hosted, billing enabled globally → billing, else role-based) aligns with the client-side logic in onboardingStore.ts and ensures consistent behavior across server and client.

packages/features/ee/organizations/lib/onboarding/__tests__/BaseOnboardingService.test.ts (3)

1-27: LGTM!

The test wrapper pattern properly exposes the protected filterTeamsAndInvites method for testing while maintaining the abstraction of BaseOnboardingService. The mock user setup is clean and follows the OnboardingUser type.


28-180: LGTM!

The test suite provides comprehensive coverage of filterTeamsAndInvites behavior, including:

  • Filtering logic for empty/whitespace values
  • Field preservation for both invites and teams
  • Optional field handling with proper undefined defaults
  • Edge cases with empty and undefined inputs

All assertions are precise and verify the expected output structure.


182-248: LGTM!

The advanced scenario tests thoroughly validate:

  • New team handling (teamId=-1) with proper role preservation
  • Mixed org-level and team-specific invites with correct field mappings
  • Complex onboarding flows with multiple teams and invite types

The detailed assertions ensure the filter logic correctly assembles both teamsData and invitedMembersData structures.

packages/features/ee/organizations/lib/onboarding/__tests__/OrganizationOnboardingFactory.test.ts (3)

1-35: LGTM!

The test setup properly manages environment state and module mocking with appropriate cleanup in afterEach. The use of vi.resetModules() ensures each test gets a fresh factory instance with the correct mock configuration.


37-117: LGTM!

The individual test cases comprehensively cover the factory's decision logic across different combinations of billing configuration, user roles, and E2E mode. Each test properly mocks dependencies and verifies the correct service type is instantiated.


134-209: LGTM!

The Factory Decision Matrix provides excellent coverage of all permutations of billing configuration, user role, and E2E mode. The parameterized approach makes the test logic clear and maintainable, ensuring the factory selects the correct service implementation in each scenario.

packages/features/ee/organizations/lib/onboarding/index.ts (1)

1-5: LGTM!

The barrel export pattern is clean and follows best practices with named exports. The module provides a clear public API surface, exporting the factory, interface, types, and concrete service implementations for downstream consumers.

packages/features/ee/organizations/lib/onboardingStore.ts (3)

1-64: LGTM!

The import changes properly use IS_TEAM_BILLING_ENABLED_CLIENT for client-side billing checks and import BillingPeriod from Prisma enums as the single source of truth. The type definitions are well-structured and the initial state is correctly typed.


66-112: LGTM!

The Zustand store implementation follows standard patterns with proper persistence configuration and a clean reset mechanism that clears localStorage when appropriate.


114-187: LGTM!

The isBillingEnabled logic correctly aligns with the server-side factory logic in OrganizationOnboardingFactory.ts, ensuring consistent behavior across client and server. The comments clearly explain the E2E mode handling and self-hosted admin exemption. The rest of the hook properly manages onboarding state synchronization with the database.

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

1-5: Good deprecation banner.

Clear migration guidance and deprecation notice look good.

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

90-94: Factory delegation LGTM.

Using OrganizationOnboardingFactory keeps handler thin and testable.

packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx (2)

13-13: Import consolidation LGTM.

Using enums from a single source is consistent.


90-95: Admin handover routing change — confirm behavior.

Previously it checked data.userId === session user. Now all admins go to handover. Confirm this is intended for admins creating orgs for others.

packages/features/ee/organizations/lib/onboarding/IOrganizationOnboardingService.ts (1)

1-32: LGTM!

The interface is well-structured with clear method signatures and comprehensive documentation. The JSDoc comments effectively explain the different behaviors for billing-enabled versus self-hosted implementations.

packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.schema.ts (2)

5-6: LGTM!

The new imports are appropriate and follow best practices by reusing shared schemas from @calcom/prisma/zod-utils.


24-25: LGTM!

The onboarding-related fields properly reuse shared schemas from @calcom/prisma/zod-utils, promoting consistency across the codebase.

packages/features/ee/organizations/lib/onboarding/__tests__/SelfHostedOnboardingService.test.ts (3)

1-54: LGTM!

The test setup comprehensively mocks all external dependencies, enabling isolated unit testing of the SelfHostedOnboardingService.


124-234: LGTM!

The test cases comprehensively cover the createOnboardingIntent flow, including edge cases like empty team names and invite emails.


311-528: LGTM!

The test suite comprehensively covers the createOrganization flow, including:

  • License validation
  • Conflict detection for slugs
  • Member invitation patterns (organization-level and team-specific)
  • Role preservation
  • Edge cases like team ownership scenarios
packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts (6)

6-6: LGTM!

The import and mock for OrganizationPaymentService align with the refactored service-based architecture.

Also applies to: 19-19


58-86: LGTM!

The mock implementation creates actual Prismock records, enabling comprehensive verification of data persistence in tests.


100-100: LGTM!

The incrementUsage mock addition aligns with the ILicenseKeyService interface and ensures the mock provides complete coverage of the service contract.

Also applies to: 290-290, 310-310


102-103: LGTM!

The Stripe environment variables enable testing of the payment integration paths.


138-139: LGTM!

The updated expectations correctly reflect the self-hosted admin flow, where organizations are created immediately (organizationId is set) without requiring payment (checkoutUrl is null).

Also applies to: 343-344


360-472: LGTM!

The new test cases comprehensively verify that teams and invited members data (including team associations and roles) are properly stored in the onboarding record.

packages/features/ee/organizations/lib/onboarding/__tests__/BillingEnabledOnboardingService.test.ts (3)

1-120: LGTM!

The test setup appropriately uses a regular user (not admin) to test the billing-enabled flow where payment is required.


122-226: LGTM!

The test cases correctly validate the billing-enabled flow, where organization creation is deferred (organizationId is null) until payment is complete, and a checkout URL is returned for payment processing.


322-747: LGTM!

The test suite comprehensively covers the billing-enabled organization creation flow, including:

  • Payment details validation
  • Idempotency handling
  • Conflict detection
  • Member invitation patterns with team-specific routing
  • Role preservation
  • Fallback behaviors
packages/features/ee/organizations/lib/onboarding/BaseOnboardingService.ts (1)

335-344: Prisma select usage LGTM

Good use of select over include, aligned with repo guidelines.

this.permissionService = permissionService || new OrganizationPermissionService(user);
}

abstract createOnboardingIntent(input: CreateOnboardingIntentInput): Promise<any>;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use precise return type (no any)

Align with IOrganizationOnboardingService: return OnboardingIntentResult, not any.

Apply:

-abstract createOnboardingIntent(input: CreateOnboardingIntentInput): Promise<any>;
+abstract createOnboardingIntent(input: CreateOnboardingIntentInput): Promise<OnboardingIntentResult>;

Also import the type:

-import type {
-  TeamInput,
-  InvitedMemberInput,
-  CreateOnboardingIntentInput,
-  OnboardingUser,
-  OrganizationData,
-  TeamData,
-  InvitedMember,
-  OrganizationOnboardingData,
-} from "./types";
+import type {
+  TeamInput,
+  InvitedMemberInput,
+  CreateOnboardingIntentInput,
+  OnboardingIntentResult,
+  OnboardingUser,
+  OrganizationData,
+  TeamData,
+  InvitedMember,
+  OrganizationOnboardingData,
+} from "./types";
📝 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
abstract createOnboardingIntent(input: CreateOnboardingIntentInput): Promise<any>;
// packages/features/ee/organizations/lib/onboarding/BaseOnboardingService.ts
import type {
TeamInput,
InvitedMemberInput,
CreateOnboardingIntentInput,
OnboardingIntentResult,
OnboardingUser,
OrganizationData,
TeamData,
InvitedMember,
OrganizationOnboardingData,
} from "./types";
export abstract class BaseOnboardingService {
// ...
// Changed return type from Promise<any> to Promise<OnboardingIntentResult>
abstract createOnboardingIntent(
input: CreateOnboardingIntentInput
): Promise<OnboardingIntentResult>;
// ...
}
🤖 Prompt for AI Agents
In packages/features/ee/organizations/lib/onboarding/BaseOnboardingService.ts at
line 66, the abstract method uses a broad `any` return type; change its
signature to return `Promise<OnboardingIntentResult>` and add an import for the
OnboardingIntentResult type from its defining module at the top of the file.
Ensure the import path matches the project structure and update any implementors
as needed to satisfy the new return type.

Comment on lines +251 to +256
await sendEmailVerification({
email: orgOwner.email,
language: orgOwner.locale ?? "en",
username: ownerProfile.username || "",
isPlatform: orgData.isPlatform,
});
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 | 🟠 Major

Feature-toggle email verification per product behavior

Email verification is now feature-toggled; gate sendEmailVerification behind the global flag (consistent with assertCanCreateOrg).

Suggested pattern:

const featuresRepository = new FeaturesRepository(prisma);
const emailVerificationEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("email-verification");
if (emailVerificationEnabled) {
  await sendEmailVerification({ ... });
}

Add import:

import { FeaturesRepository } from "@calcom/lib/server/repository/features";
🤖 Prompt for AI Agents
In packages/features/ee/organizations/lib/onboarding/BaseOnboardingService.ts
around lines 251 to 256, the call to sendEmailVerification must be gated by the
global "email-verification" feature flag; import FeaturesRepository from
"@calcom/lib/server/repository/features", instantiate it with the existing
prisma instance, call checkIfFeatureIsEnabledGlobally("email-verification"), and
only call sendEmailVerification when that returns true (retain the same
payload). Ensure the new import is added at the top of the file and any
async/await usage follows the surrounding error-handling pattern.

Comment on lines +323 to +333
log.info(
`Processing ${invitedMembers.length} member invites for organization ${organization.id}`,
safeStringify({
invitedMembers: invitedMembers.map((m) => ({
email: m.email,
teamId: m.teamId,
teamName: m.teamName,
role: m.role,
})),
})
);
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 | 🟠 Major

Avoid logging PII (emails) at info level

Emails are logged at info level. This is PII and a compliance risk. Redact or downgrade to debug with hashing, or log counts only.

Minimal change:

  • Replace emails with counts, or
  • Use logger.debug and mask values:
log.debug("Invites", safeStringify({
  invitedMembers: invitedMembers.map((m) => ({ emailHash: hash(m.email), ... }))
}));

Also applies to: 400-411

🤖 Prompt for AI Agents
In packages/features/ee/organizations/lib/onboarding/BaseOnboardingService.ts
around lines 323-333 (and similarly 400-411), the current info-level log emits
raw email addresses (PII). Change these logs to avoid PII by either logging only
counts/team metadata at info level or demoting to debug and replacing email with
a non-reversible hash or masked value; e.g., use log.debug with safeStringify
and map invitedMembers to include emailHash or maskedEmail instead of the raw
email, or simply log invitedMembers.length and team/role details without any
email field at info level.

Comment on lines +422 to +424
usernameOrEmail: member.email,
role: (member.role as MembershipRole) || MembershipRole.MEMBER,
})),
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 | 🟠 Major

Harden role mapping for invites (no lax casts)

(member.role as MembershipRole) || MEMBER can pass invalid strings. Map explicitly to allowed roles.

-          role: (member.role as MembershipRole) || MembershipRole.MEMBER,
+          role:
+            member.role?.toUpperCase() === "ADMIN"
+              ? MembershipRole.ADMIN
+              : MembershipRole.MEMBER,

Apply to both org- and team-level invitations.

Also applies to: 438-441

🤖 Prompt for AI Agents
In packages/features/ee/organizations/lib/onboarding/BaseOnboardingService.ts
around lines 422-424 (and similarly 438-441), the code uses a lax cast
"(member.role as MembershipRole) || MembershipRole.MEMBER" which can allow
invalid strings; replace this with an explicit mapping/validation: check
member.role against the allowed MembershipRole values (e.g., via a switch, a Set
of valid role strings, or a helper function like
mapToMembershipRole(member.role)) and return the matched MembershipRole or
default to MembershipRole.MEMBER; apply the same explicit mapping for both
org-level and team-level invitation mappings to ensure only valid enum values
are used.

Comment on lines +149 to +164
const orgData: OrganizationData = {
id: organizationOnboarding.organizationId,
name: organizationOnboarding.name,
slug: organizationOnboarding.slug,
isOrganizationConfigured: true,
isOrganizationAdminReviewed: true,
autoAcceptEmail: organizationOnboarding.orgOwnerEmail.split("@")[1],
seats: organizationOnboarding.seats,
pricePerSeat: organizationOnboarding.pricePerSeat,
isPlatform: false,
billingPeriod: organizationOnboarding.billingPeriod,
logoUrl: organizationOnboarding.logo,
bio: organizationOnboarding.bio,
brandColor: organizationOnboarding.brandColor,
bannerUrl: organizationOnboarding.bannerUrl,
};
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 | 🔴 Critical

🧩 Analysis chain

Do not hard‑code isPlatform to false.

Use the value from onboarding to avoid silently dropping platform orgs.

-      isPlatform: false,
+      isPlatform: organizationOnboarding.isPlatform,

🏁 Script executed:

#!/bin/bash
# Show occurrences of organizationOnboarding and its fetch in SelfHostedOnboardingService.ts
rg -nC3 "organizationOnboarding" -g "packages/features/ee/organizations/lib/onboarding/SelfHostedOnboardingService.ts"

# Inspect how organizationOnboarding is retrieved (ensure isPlatform is selected)
rg -nC3 "findUnique" -g "packages/features/ee/organizations/lib/onboarding/SelfHostedOnboardingService.ts"

Length of output: 14488


🏁 Script executed:

#!/bin/bash
# Show file header to locate createOnboardingRecord and type defs
sed -n '1,100p' packages/features/ee/organizations/lib/onboarding/SelfHostedOnboardingService.ts

# Search for OrganizationOnboardingData type usage/definition
rg -n "OrganizationOnboardingData" -g "packages/features/ee/organizations/lib/onboarding/SelfHostedOnboardingService.ts"

Length of output: 4190


Use the persisted isPlatform value rather than hard-coding false.

-      isPlatform: false,
+      isPlatform: organizationOnboarding.isPlatform,
📝 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
const orgData: OrganizationData = {
id: organizationOnboarding.organizationId,
name: organizationOnboarding.name,
slug: organizationOnboarding.slug,
isOrganizationConfigured: true,
isOrganizationAdminReviewed: true,
autoAcceptEmail: organizationOnboarding.orgOwnerEmail.split("@")[1],
seats: organizationOnboarding.seats,
pricePerSeat: organizationOnboarding.pricePerSeat,
isPlatform: false,
billingPeriod: organizationOnboarding.billingPeriod,
logoUrl: organizationOnboarding.logo,
bio: organizationOnboarding.bio,
brandColor: organizationOnboarding.brandColor,
bannerUrl: organizationOnboarding.bannerUrl,
};
const orgData: OrganizationData = {
id: organizationOnboarding.organizationId,
name: organizationOnboarding.name,
slug: organizationOnboarding.slug,
isOrganizationConfigured: true,
isOrganizationAdminReviewed: true,
autoAcceptEmail: organizationOnboarding.orgOwnerEmail.split("@")[1],
seats: organizationOnboarding.seats,
pricePerSeat: organizationOnboarding.pricePerSeat,
isPlatform: organizationOnboarding.isPlatform,
billingPeriod: organizationOnboarding.billingPeriod,
logoUrl: organizationOnboarding.logo,
bio: organizationOnboarding.bio,
brandColor: organizationOnboarding.brandColor,
bannerUrl: organizationOnboarding.bannerUrl,
};
🤖 Prompt for AI Agents
In
packages/features/ee/organizations/lib/onboarding/SelfHostedOnboardingService.ts
around lines 149 to 164, the OrganizationData object hard-codes isPlatform:
false; replace that with the persisted value from the organizationOnboarding
record (e.g., isPlatform: organizationOnboarding.isPlatform) so the true/false
setting is preserved; if that property may be undefined, use a clear fallback
(e.g., ?? false) to maintain typing and behavior.

Comment on lines +47 to +49
const emailParts = email.split("@");
if (emailParts.length < 2) return false;
return !personalEmailProviders.includes(emailParts[1]);
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 | 🔴 Critical

Critical: Case-insensitive domain comparison required.

The domain comparison on line 49 is case-sensitive, causing emails like [email protected] or [email protected] to incorrectly return true (company email). Email domains are case-insensitive per RFC specifications.

Apply this diff to fix the case sensitivity issue:

 const emailParts = email.split("@");
 if (emailParts.length < 2) return false;
-return !personalEmailProviders.includes(emailParts[1]);
+return !personalEmailProviders.includes(emailParts[1].toLowerCase());
📝 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
const emailParts = email.split("@");
if (emailParts.length < 2) return false;
return !personalEmailProviders.includes(emailParts[1]);
const emailParts = email.split("@");
if (emailParts.length < 2) return false;
return !personalEmailProviders.includes(emailParts[1].toLowerCase());
🤖 Prompt for AI Agents
In packages/features/ee/organizations/lib/utils.ts around lines 47 to 49, the
domain check is case-sensitive and will misclassify domains like "Gmail.com";
normalize the domain to lowercase (and trim any surrounding whitespace) before
checking against personalEmailProviders, e.g. compute domain =
emailParts[1].toLowerCase().trim() and use that for the includes() check so the
comparison is case-insensitive.

Comment on lines +77 to 80
const organizationOnboarding = await OrganizationOnboardingRepository.findByOrgOwnerEmail(orgOwner.email);
if (organizationOnboarding) {
throw new Error("organization_onboarding_already_exists");
}
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 | 🟠 Major

Use TRPCError and only block when an active/incomplete onboarding exists.

Throwing a generic Error loses proper client codes. Also, allow creating a new onboarding if the previous one is complete.

-  const organizationOnboarding = await OrganizationOnboardingRepository.findByOrgOwnerEmail(orgOwner.email);
-  if (organizationOnboarding) {
-    throw new Error("organization_onboarding_already_exists");
-  }
+  const organizationOnboarding =
+    await OrganizationOnboardingRepository.findByOrgOwnerEmail(orgOwner.email);
+  if (organizationOnboarding && !organizationOnboarding.isComplete) {
+    throw new TRPCError({
+      code: "CONFLICT",
+      message: "organization_onboarding_already_exists",
+    });
+  }
📝 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
const organizationOnboarding = await OrganizationOnboardingRepository.findByOrgOwnerEmail(orgOwner.email);
if (organizationOnboarding) {
throw new Error("organization_onboarding_already_exists");
}
const organizationOnboarding =
await OrganizationOnboardingRepository.findByOrgOwnerEmail(orgOwner.email);
if (organizationOnboarding && !organizationOnboarding.isComplete) {
throw new TRPCError({
code: "CONFLICT",
message: "organization_onboarding_already_exists",
});
}
🤖 Prompt for AI Agents
In
packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.ts
around lines 77-80, replace the generic Error throw with a TRPCError and only
block when the found onboarding is still active/incomplete; import TRPCError
from @trpc/server, check a definitive completion flag on organizationOnboarding
(e.g., status !== 'complete' or completedAt is null/undefined) and if it's not
completed throw new TRPCError({ code: 'CONFLICT', message:
'organization_onboarding_already_exists' }) so completed onboards do not block
new creation.

Copy link
Contributor

@eunjae-lee eunjae-lee left a comment

Choose a reason for hiding this comment

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

The change looks good overall ! Let me dive into testing it.

onboardingId: organizationOnboarding.id,
})
) {
throw new Error("organization_already_exists_with_this_slug");
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: is this a sort of code that is mapped somewhere else? or supposed to be just a message?

Suggested change
throw new Error("organization_already_exists_with_this_slug");
throw new Error("Organization already exists with this slug");

Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure - I just moved this into the class from the old method. Will take a look

organizationId: organization.id,
});

const teamsData = teamsSchema.parse(organizationOnboarding.teams);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Do we need to care if parsing fails? I wonder if we can have organizationOnboarding.teams or organizationOnboarding.invitedMembers typed properly instead of Prisma.JsonValue (which means it should be already parsed before being passed to createOrganization method.

export function isCompanyEmail(email: string): boolean {
// A list of popular @domains that are personal email providers
const personalEmailProviders = [
"gmail.com",
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we have similar lists across the code base. Can we take this opportunity extract them as a const?

Screenshot 2025-10-13 at 16 31 38

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

consumer core area: core, team members only ❗️ migrations contains migration files ready-for-e2e 💻 refactor size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants