-
Notifications
You must be signed in to change notification settings - Fork 10.8k
chore: organization onboarding refactor #24381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Server Action for gettting slug satus of org
packages/features/ee/organizations/lib/onboarding/BillingEnabledOnboardingService.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
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
brandColor: input.brandColor ?? null, | ||
bannerUrl: input.bannerUrl ?? null, |
There was a problem hiding this comment.
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[] = []) { |
There was a problem hiding this comment.
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
…nboarding-refactor' into feat/organziation-onboarding-refactor
E2E results are ready! |
packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx
Show resolved
Hide resolved
…nboarding-refactor' into feat/organziation-onboarding-refactor
There was a problem hiding this 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, aSet
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 domainapps/web/playwright/organization/organization-creation.e2e.ts (1)
467-471
: Gate on field visibility to avoid flag/UI divergence in E2EImported 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 formatpackages/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 codesbannerUrl
: Use.url()
to ensure valid URL formatExample:
- 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 ambiguityUsing 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 clearNice 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 checksNow 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 couplingSince 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 importintentToCreateOrgHandler
by name, so you can safely delete the finalexport 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
andbannerUrl
: validate URL format with.url()
or custom regexbrandColor
: 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 theBillingPeriod.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 constantCreationSource.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 recordInput 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 rolesUse 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 creationIf 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 valuesAlign 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 typingUse 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.
📒 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 useinclude
, always useselect
Ensure thecredential.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 featurepackages/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 tonull
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
isundefined
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 toTeamFeatures
throughkeyof AppFlags
.packages/features/ee/billing/api/webhook/_invoice.paid.org.ts (1)
116-120
: Guard against missing subscription item idcreateOrganization 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: LGTMTyped context aligns with OrganizationPaymentService changes.
81-83
: Forwarding branding fields: LGTMbrandColor 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: LGTMPayload 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: LGTMEnsure 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): LGTMAlso applies to: 32-44
81-87
: Constructor now uses OnboardingUser: LGTMKeeps 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: LGTMConfirm 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 ofBaseOnboardingService
. The mock user setup is clean and follows theOnboardingUser
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
andinvitedMembersData
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 ofvi.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 importBillingPeriod
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 inOrganizationOnboardingFactory.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 theILicenseKeyService
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 LGTMGood use of
select
overinclude
, aligned with repo guidelines.
this.permissionService = permissionService || new OrganizationPermissionService(user); | ||
} | ||
|
||
abstract createOnboardingIntent(input: CreateOnboardingIntentInput): Promise<any>; |
There was a problem hiding this comment.
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.
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.
await sendEmailVerification({ | ||
email: orgOwner.email, | ||
language: orgOwner.locale ?? "en", | ||
username: ownerProfile.username || "", | ||
isPlatform: orgData.isPlatform, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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, | ||
})), | ||
}) | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
usernameOrEmail: member.email, | ||
role: (member.role as MembershipRole) || MembershipRole.MEMBER, | ||
})), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
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.
const emailParts = email.split("@"); | ||
if (emailParts.length < 2) return false; | ||
return !personalEmailProviders.includes(emailParts[1]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
const organizationOnboarding = await OrganizationOnboardingRepository.findByOrgOwnerEmail(orgOwner.email); | ||
if (organizationOnboarding) { | ||
throw new Error("organization_onboarding_already_exists"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
There was a problem hiding this 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"); |
There was a problem hiding this comment.
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?
throw new Error("organization_already_exists_with_this_slug"); | |
throw new Error("Organization already exists with this slug"); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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