@@ -305,7 +306,7 @@ const CreateANewOrganizationFormChild = ({ session }: { session: Ensure
diff --git a/packages/features/ee/organizations/lib/onboardingStore.ts b/packages/features/ee/organizations/lib/onboardingStore.ts
index aa590d19d6..3dd9210aca 100644
--- a/packages/features/ee/organizations/lib/onboardingStore.ts
+++ b/packages/features/ee/organizations/lib/onboardingStore.ts
@@ -4,7 +4,7 @@ import { useEffect } from "react";
import { create } from "zustand";
import { persist } from "zustand/middleware";
-import { WEBAPP_URL } from "@calcom/lib/constants";
+import { WEBAPP_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
@@ -102,6 +102,10 @@ export const useOnboardingStore = create()(
if (state) {
set(state);
} else {
+ // Clear the localStorage entry for this store
+ if (typeof window !== "undefined") {
+ localStorage.removeItem("org-creation-onboarding");
+ }
set(initialState);
}
},
@@ -117,6 +121,7 @@ export const useOnboarding = (params?: { step?: "start" | "status" | null }) =>
const router = useRouter();
const path = usePathname();
const isAdmin = session.data?.user?.role === UserPermissionRole.ADMIN;
+ const isBillingEnabled = !(IS_SELF_HOSTED && isAdmin) || process.env.NEXT_PUBLIC_IS_E2E;
const searchParams = useSearchParams();
const { data: organizationOnboarding, isPending: isLoadingOrgOnboarding } =
trpc.viewer.organizations.getOrganizationOnboarding.useQuery();
@@ -127,6 +132,10 @@ export const useOnboarding = (params?: { step?: "start" | "status" | null }) =>
return;
}
+ if (organizationOnboarding?.isComplete) {
+ reset();
+ }
+
if (organizationOnboarding) {
// Must not keep resetting state on each step change as every step doesn't save at the moment and this would reset user's changes
if (!window.isOrgOnboardingSynced) {
@@ -143,6 +152,9 @@ export const useOnboarding = (params?: { step?: "start" | "status" | null }) =>
bio: organizationOnboarding.bio,
logo: organizationOnboarding.logo,
});
+ if (isAdmin && organizationOnboarding?.orgOwnerEmail !== session.data?.user.email) {
+ reset();
+ }
}
} else {
// First step doesn't require onboardingId
@@ -154,11 +166,6 @@ export const useOnboarding = (params?: { step?: "start" | "status" | null }) =>
router.push("/settings/organizations/new");
}
}
-
- // Admin must start fresh, so that they can create organization with any email and data doesn't mix from different org creations
- if (isAdmin) {
- reset();
- }
}, [organizationOnboarding, isLoadingOrgOnboarding, isAdmin, onboardingId, reset, step, router]);
useEffect(() => {
@@ -170,5 +177,10 @@ export const useOnboarding = (params?: { step?: "start" | "status" | null }) =>
router.push(`/auth/login?callbackUrl=${WEBAPP_URL}${path}${searchString ? `?${searchString}` : ""}`);
}
}, [session, router, path, searchParams]);
- return { useOnboardingStore, isLoadingOrgOnboarding, dbOnboarding: organizationOnboarding };
+ return {
+ useOnboardingStore,
+ isLoadingOrgOnboarding,
+ dbOnboarding: organizationOnboarding,
+ isBillingEnabled,
+ };
};
diff --git a/packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.test.ts b/packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.test.ts
index 0f6e8d4500..f32cdfed2f 100644
--- a/packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.test.ts
+++ b/packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.test.ts
@@ -6,13 +6,22 @@ import prismock from "../../../../../../tests/libs/__mocks__/prisma";
import { describe, expect, it, vi, beforeEach } from "vitest";
+import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService";
+import * as constants from "@calcom/lib/constants";
import { createDomain } from "@calcom/lib/domainManager/organization";
+import type { OrganizationOnboarding } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { createTeamsHandler } from "@calcom/trpc/server/routers/viewer/organizations/createTeams.handler";
import { inviteMembersWithNoInviterPermissionCheck } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler";
import { createOrganizationFromOnboarding } from "./createOrganizationFromOnboarding";
+vi.mock("@calcom/ee/common/server/LicenseKeyService", () => ({
+ LicenseKeySingleton: {
+ getInstance: vi.fn(),
+ },
+}));
+
// Mock the external functions
vi.mock("@calcom/features/auth/lib/verifyEmail", () => ({
sendEmailVerification: vi.fn(),
@@ -147,6 +156,19 @@ async function createTestMembership(data: { userId: number; teamId: number; role
});
}
+async function expectOrganizationOnboardingToHaveDataContaining({
+ onboardingId,
+ expectedData,
+}: {
+ onboardingId: string;
+ expectedData: Partial;
+}) {
+ const updatedOrganizationOnboarding = await prismock.organizationOnboarding.findUnique({
+ where: { id: onboardingId },
+ });
+ expect(updatedOrganizationOnboarding).toEqual(expect.objectContaining(expectedData));
+}
+
describe("createOrganizationFromOnboarding", () => {
beforeEach(async () => {
vi.resetAllMocks();
@@ -154,260 +176,326 @@ describe("createOrganizationFromOnboarding", () => {
await prismock.reset();
});
- // Skipped because non-existend user creation support isn't there through onboarding now.
- it.skip("should create an organization with a non-existent user as owner", async () => {
- const organizationOnboarding = await createTestOrganizationOnboarding();
-
- const { organization, owner } = await createOrganizationFromOnboarding({
- organizationOnboarding,
- paymentSubscriptionId: "mock_subscription_id",
- paymentSubscriptionItemId: "mock_subscription_item_id",
+ describe("hosted", () => {
+ beforeEach(() => {
+ vi.spyOn(constants, "IS_SELF_HOSTED", "get").mockReturnValue(false);
});
- // Verify organization creation
- expect(organization).toBeDefined();
- expect(organization.name).toBe(organizationOnboarding.name);
- expect(organization.slug).toBe(organizationOnboarding.slug);
- expect(organization.metadata).toEqual({
- orgSeats: 5,
- orgPricePerSeat: 20,
- billingPeriod: "MONTHLY",
- subscriptionId: "mock_subscription_id",
- subscriptionItemId: "mock_subscription_item_id",
+ // Skipped because non-existend user creation support isn't there through onboarding now.
+ it.skip("should create an organization with a non-existent user as owner", async () => {
+ const organizationOnboarding = await createTestOrganizationOnboarding();
+
+ const { organization, owner } = await createOrganizationFromOnboarding({
+ organizationOnboarding,
+ paymentSubscriptionId: "mock_subscription_id",
+ paymentSubscriptionItemId: "mock_subscription_item_id",
+ });
+
+ // Verify organization creation
+ expect(organization).toBeDefined();
+ expect(organization.name).toBe(organizationOnboarding.name);
+ expect(organization.slug).toBe(organizationOnboarding.slug);
+ expect(organization.metadata).toEqual({
+ orgSeats: 5,
+ orgPricePerSeat: 20,
+ billingPeriod: "MONTHLY",
+ subscriptionId: "mock_subscription_id",
+ subscriptionItemId: "mock_subscription_item_id",
+ });
+
+ // Verify owner creation
+ expect(owner).toBeDefined();
+ expect(owner.email).toBe(organizationOnboarding.orgOwnerEmail);
});
- // Verify owner creation
- expect(owner).toBeDefined();
- expect(owner.email).toBe(organizationOnboarding.orgOwnerEmail);
- });
-
- it("should create an organization with an existing user as owner", async () => {
- const orgOwnerEmail = "test@example.com";
- const existingUser = await createTestUser({
- email: orgOwnerEmail,
- name: "Existing User",
- username: "existinguser",
- onboardingCompleted: true,
- emailVerified: new Date(),
- });
-
- const organizationOnboarding = await createTestOrganizationOnboarding({
- orgOwnerEmail,
- });
-
- const { organization, owner } = await createOrganizationFromOnboarding({
- organizationOnboarding,
- paymentSubscriptionId: "mock_subscription_id",
- paymentSubscriptionItemId: "mock_subscription_item_id",
- });
-
- // Verify organization creation
- expect(organization).toBeDefined();
- expect(organization.name).toBe(organizationOnboarding.name);
- expect(organization.slug).toBe(organizationOnboarding.slug);
-
- // Verify owner is the existing user
- expect(owner.id).toBe(existingUser.id);
- expect(owner.email).toBe(existingUser.email);
-
- expect(createDomain).toHaveBeenCalledWith(organization.slug);
- // Verify team creation and member invites still work
- expect(inviteMembersWithNoInviterPermissionCheck).toHaveBeenCalledWith(
- expect.objectContaining({
- orgSlug: organization.slug,
- teamId: organization.id,
- invitations: organizationOnboarding.invitedMembers?.map((member) => ({
- usernameOrEmail: member.email,
- role: MembershipRole.MEMBER,
- })),
- })
- );
- expect(createTeamsHandler).toHaveBeenCalledWith(
- expect.objectContaining({
- input: expect.objectContaining({
- orgId: organization.id,
- teamNames: ["New Team"],
- moveTeams: expect.arrayContaining([expect.objectContaining({ id: 1, newSlug: "new-team-slug" })]),
- }),
- })
- );
- });
-
- it("should reuse existing organization if organizationId is already set in onboarding. Covers the retry scenario where org got created but then error occurred", async () => {
- const orgName = "Existing Org";
- const orgSlug = "existing-org";
- const existingOrg = await createTestOrganization({
- name: orgName,
- slug: orgSlug,
- });
-
- const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
- user: {
+ it("should create an organization with an existing user as owner", async () => {
+ const orgOwnerEmail = "test@example.com";
+ const existingUser = await createTestUser({
+ email: orgOwnerEmail,
name: "Existing User",
username: "existinguser",
- email: "test@example.com",
- },
- organizationOnboarding: {
- organizationId: existingOrg.id,
+ onboardingCompleted: true,
+ emailVerified: new Date(),
+ });
+
+ const organizationOnboarding = await createTestOrganizationOnboarding({
+ orgOwnerEmail,
+ });
+
+ const { organization, owner } = await createOrganizationFromOnboarding({
+ organizationOnboarding,
+ paymentSubscriptionId: "mock_subscription_id",
+ paymentSubscriptionItemId: "mock_subscription_item_id",
+ });
+
+ // Verify organization creation
+ expect(organization).toBeDefined();
+ expect(organization.name).toBe(organizationOnboarding.name);
+ expect(organization.slug).toBe(organizationOnboarding.slug);
+
+ // Verify owner is the existing user
+ expect(owner.id).toBe(existingUser.id);
+ expect(owner.email).toBe(existingUser.email);
+
+ expect(createDomain).toHaveBeenCalledWith(organization.slug);
+ // Verify team creation and member invites still work
+ expect(inviteMembersWithNoInviterPermissionCheck).toHaveBeenCalledWith(
+ expect.objectContaining({
+ orgSlug: organization.slug,
+ teamId: organization.id,
+ invitations: organizationOnboarding.invitedMembers?.map((member) => ({
+ usernameOrEmail: member.email,
+ role: MembershipRole.MEMBER,
+ })),
+ })
+ );
+ expect(createTeamsHandler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ input: expect.objectContaining({
+ orgId: organization.id,
+ teamNames: ["New Team"],
+ moveTeams: expect.arrayContaining([expect.objectContaining({ id: 1, newSlug: "new-team-slug" })]),
+ }),
+ })
+ );
+ });
+
+ it("should reuse existing organization if organizationId is already set in onboarding. Covers the retry scenario where org got created but then error occurred", async () => {
+ const orgName = "Existing Org";
+ const orgSlug = "existing-org";
+ const existingOrg = await createTestOrganization({
name: orgName,
slug: orgSlug,
- },
- });
+ });
- const { organization } = await createOrganizationFromOnboarding({
- organizationOnboarding,
- paymentSubscriptionId: "mock_subscription_id",
- paymentSubscriptionItemId: "mock_subscription_item_id",
- });
+ const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
+ user: {
+ name: "Existing User",
+ username: "existinguser",
+ email: "test@example.com",
+ },
+ organizationOnboarding: {
+ organizationId: existingOrg.id,
+ name: orgName,
+ slug: orgSlug,
+ },
+ });
- // Verify the existing organization was reused
- expect(organization.id).toBe(existingOrg.id);
- });
-
- it("should throw error if organization with same slug exists", async () => {
- const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
- user: {
- email: "test@example.com",
- },
- organizationOnboarding: {
- slug: "conflicting-org-with-same-slug",
- },
- });
-
- await createTestOrganization({
- name: "Conflicting Org with same slug",
- slug: organizationOnboarding.slug,
- });
-
- await expect(
- createOrganizationFromOnboarding({
+ const { organization } = await createOrganizationFromOnboarding({
organizationOnboarding,
paymentSubscriptionId: "mock_subscription_id",
paymentSubscriptionItemId: "mock_subscription_item_id",
- })
- ).rejects.toThrow("organization_url_taken");
- });
+ });
- it("should create team with slugified slug", async () => {
- const teamToMove = await createTestTeam({
- name: "Sales",
- slug: "Company Sales",
+ // Verify the existing organization was reused
+ expect(organization.id).toBe(existingOrg.id);
});
- const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
- user: {
- email: "test@example.com",
- },
- organizationOnboarding: {
- slug: "UPPERCASE-SLUG",
- teams: [{ id: teamToMove.id, name: "Sales", isBeingMigrated: true, slug: "sales-team" }],
- },
+ it("should throw error if organization with same slug exists", async () => {
+ const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
+ user: {
+ email: "test@example.com",
+ },
+ organizationOnboarding: {
+ slug: "conflicting-org-with-same-slug",
+ },
+ });
+
+ await createTestOrganization({
+ name: "Conflicting Org with same slug",
+ slug: organizationOnboarding.slug,
+ });
+
+ await expect(
+ createOrganizationFromOnboarding({
+ organizationOnboarding,
+ paymentSubscriptionId: "mock_subscription_id",
+ paymentSubscriptionItemId: "mock_subscription_item_id",
+ })
+ ).rejects.toThrow("organization_url_taken");
});
- const { organization } = await createOrganizationFromOnboarding({
- organizationOnboarding,
- paymentSubscriptionId: "mock_subscription_id",
- paymentSubscriptionItemId: "mock_subscription_item_id",
- });
+ it("should create team with slugified slug", async () => {
+ const teamToMove = await createTestTeam({
+ name: "Sales",
+ slug: "Company Sales",
+ });
- expect(createTeamsHandler).toHaveBeenCalledWith(
- expect.objectContaining({
- input: expect.objectContaining({
- orgId: organization.id,
- teamNames: [],
- moveTeams: expect.arrayContaining([expect.objectContaining({ id: 1, newSlug: "sales-team" })]),
- }),
- })
- );
- });
+ const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
+ user: {
+ email: "test@example.com",
+ },
+ organizationOnboarding: {
+ slug: "UPPERCASE-SLUG",
+ teams: [{ id: teamToMove.id, name: "Sales", isBeingMigrated: true, slug: "sales-team" }],
+ },
+ });
- it("should update stripe customer ID for existing user", async () => {
- const { user: existingUser, organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
- user: {
- email: "test@example.com",
- },
- organizationOnboarding: {
- slug: "conflicting-org-with-same-slug",
- },
- });
-
- await createOrganizationFromOnboarding({
- organizationOnboarding,
- paymentSubscriptionId: "mock_subscription_id",
- paymentSubscriptionItemId: "mock_subscription_item_id",
- });
-
- // Verify user's stripe customer ID was updated
- const updatedUser = await prismock.user.findUnique({
- where: { id: existingUser.id },
- });
-
- expect(updatedUser?.metadata).toEqual(
- expect.objectContaining({
- stripeCustomerId: organizationOnboarding.stripeCustomerId,
- })
- );
- });
-
- it("should create organization in with slug set even if there is a team with same slug of which orgOwner(to-be) is a member", async () => {
- const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
- user: {
- email: "test@example.com",
- },
- organizationOnboarding: {
- slug: "conflicting-org-with-same-slug",
- },
- });
-
- const teamWithConflictingSlug = await createTestTeam({
- name: "TestTeamWithConflictingSlug",
- slug: organizationOnboarding.slug,
- });
-
- // Make the orgOwner a member of the team
- await createTestMembership({
- userId: organizationOnboarding.createdById,
- teamId: teamWithConflictingSlug.id,
- role: MembershipRole.ADMIN,
- });
-
- const { organization } = await createOrganizationFromOnboarding({
- organizationOnboarding,
- paymentSubscriptionId: "mock_subscription_id",
- paymentSubscriptionItemId: "mock_subscription_item_id",
- });
-
- expect(organization.slug).toBe(organizationOnboarding.slug);
- });
-
- it("should not create organization if there is a team with same slug of which orgOwner(to-be) is not a member", async () => {
- const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
- user: {
- email: "test@example.com",
- },
- organizationOnboarding: {
- slug: "conflicting-org-with-same-slug",
- },
- });
-
- await createTestTeam({
- name: "TestTeamWithConflictingSlugNotOwnedByOrgOwner",
- slug: organizationOnboarding.slug,
- });
-
- // Don't make the orgOwner a member of the team
- // await createTestMembership({
- // userId: organizationOnboarding.createdById,
- // teamId: teamWithConflictingSlug.id,
- // role: MembershipRole.ADMIN,
- // });
-
- await expect(
- createOrganizationFromOnboarding({
+ const { organization } = await createOrganizationFromOnboarding({
organizationOnboarding,
paymentSubscriptionId: "mock_subscription_id",
paymentSubscriptionItemId: "mock_subscription_item_id",
- })
- ).rejects.toThrow("organization_url_taken");
+ });
+
+ expect(createTeamsHandler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ input: expect.objectContaining({
+ orgId: organization.id,
+ teamNames: [],
+ moveTeams: expect.arrayContaining([expect.objectContaining({ id: 1, newSlug: "sales-team" })]),
+ }),
+ })
+ );
+ });
+
+ it("should update stripe customer ID for existing user", async () => {
+ const { user: existingUser, organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
+ user: {
+ email: "test@example.com",
+ },
+ organizationOnboarding: {
+ slug: "conflicting-org-with-same-slug",
+ },
+ });
+
+ await createOrganizationFromOnboarding({
+ organizationOnboarding,
+ paymentSubscriptionId: "mock_subscription_id",
+ paymentSubscriptionItemId: "mock_subscription_item_id",
+ });
+
+ // Verify user's stripe customer ID was updated
+ const updatedUser = await prismock.user.findUnique({
+ where: { id: existingUser.id },
+ });
+
+ expect(updatedUser?.metadata).toEqual(
+ expect.objectContaining({
+ stripeCustomerId: organizationOnboarding.stripeCustomerId,
+ })
+ );
+ });
+
+ it("should create organization in with slug set even if there is a team with same slug of which orgOwner(to-be) is a member", async () => {
+ const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
+ user: {
+ email: "test@example.com",
+ },
+ organizationOnboarding: {
+ slug: "conflicting-org-with-same-slug",
+ },
+ });
+
+ const teamWithConflictingSlug = await createTestTeam({
+ name: "TestTeamWithConflictingSlug",
+ slug: organizationOnboarding.slug,
+ });
+
+ // Make the orgOwner a member of the team
+ await createTestMembership({
+ userId: organizationOnboarding.createdById,
+ teamId: teamWithConflictingSlug.id,
+ role: MembershipRole.ADMIN,
+ });
+
+ const { organization } = await createOrganizationFromOnboarding({
+ organizationOnboarding,
+ paymentSubscriptionId: "mock_subscription_id",
+ paymentSubscriptionItemId: "mock_subscription_item_id",
+ });
+
+ expect(organization.slug).toBe(organizationOnboarding.slug);
+ });
+
+ it("should not create organization if there is a team with same slug of which orgOwner(to-be) is not a member", async () => {
+ const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
+ user: {
+ email: "test@example.com",
+ },
+ organizationOnboarding: {
+ slug: "conflicting-org-with-same-slug",
+ },
+ });
+
+ await createTestTeam({
+ name: "TestTeamWithConflictingSlugNotOwnedByOrgOwner",
+ slug: organizationOnboarding.slug,
+ });
+
+ // Don't make the orgOwner a member of the team
+ // await createTestMembership({
+ // userId: organizationOnboarding.createdById,
+ // teamId: teamWithConflictingSlug.id,
+ // role: MembershipRole.ADMIN,
+ // });
+
+ await expect(
+ createOrganizationFromOnboarding({
+ organizationOnboarding,
+ paymentSubscriptionId: "mock_subscription_id",
+ paymentSubscriptionItemId: "mock_subscription_item_id",
+ })
+ ).rejects.toThrow("organization_url_taken");
+ });
+
+ it("should not mark the Onboarding as completed(isComplete=true)", async () => {
+ const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
+ user: {
+ email: "test@example.com",
+ },
+ });
+
+ await expectOrganizationOnboardingToHaveDataContaining({
+ onboardingId: organizationOnboarding.id,
+ expectedData: {
+ isComplete: false,
+ },
+ });
+ });
+ });
+
+ describe("self-hosted", () => {
+ beforeEach(() => {
+ vi.spyOn(constants, "IS_SELF_HOSTED", "get").mockReturnValue(true);
+ });
+
+ it("should fail if the license is invalid", async () => {
+ vi.mocked(LicenseKeySingleton.getInstance as ReturnType).mockResolvedValue({
+ checkLicense: vi.fn().mockResolvedValue(false),
+ });
+ const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
+ user: {
+ email: "test@example.com",
+ },
+ });
+
+ await expect(
+ createOrganizationFromOnboarding({
+ organizationOnboarding,
+ paymentSubscriptionId: "mock_subscription_id",
+ paymentSubscriptionItemId: "mock_subscription_item_id",
+ })
+ ).rejects.toThrow("Self hosted license not valid");
+ });
+
+ it("should create an organization with a valid license", async () => {
+ vi.mocked(LicenseKeySingleton.getInstance).mockResolvedValue({
+ checkLicense: vi.fn().mockResolvedValue(true),
+ });
+ const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
+ user: {
+ email: "test@example.com",
+ },
+ });
+
+ const { organization, owner } = await createOrganizationFromOnboarding({
+ organizationOnboarding,
+ paymentSubscriptionId: "mock_subscription_id",
+ paymentSubscriptionItemId: "mock_subscription_item_id",
+ });
+
+ expect(organization).toBeDefined();
+ expect(owner).toBeDefined();
+ });
});
});
diff --git a/packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.ts b/packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.ts
index 6bb139e631..e697fcc24c 100644
--- a/packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.ts
+++ b/packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.ts
@@ -1,5 +1,6 @@
import type { TFunction } from "i18next";
+import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService";
import { sendOrganizationCreationEmail } from "@calcom/emails/email-manager";
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
@@ -10,9 +11,11 @@ import {
} from "@calcom/features/ee/organizations/lib/server/orgCreationUtils";
import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
import { getAvailabilityFromSchedule } from "@calcom/lib/availability";
+import { IS_SELF_HOSTED } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { getTranslation } from "@calcom/lib/server/i18n";
+import { DeploymentRepository } from "@calcom/lib/server/repository/deployment";
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
import { OrganizationOnboardingRepository } from "@calcom/lib/server/repository/organizationOnboarding";
import { UserRepository } from "@calcom/lib/server/repository/user";
@@ -333,9 +336,13 @@ async function backwardCompatibilityForSubscriptionDetails({
id: number;
metadata: Prisma.JsonValue;
};
- paymentSubscriptionId: string;
- paymentSubscriptionItemId: string;
+ paymentSubscriptionId?: string;
+ paymentSubscriptionItemId?: string;
}) {
+ if (!paymentSubscriptionId || !paymentSubscriptionItemId) {
+ return organization;
+ }
+
const existingMetadata = teamMetadataSchema.parse(organization.metadata);
const updatedOrganization = await OrganizationRepository.updateStripeSubscriptionDetails({
id: organization.id,
@@ -387,9 +394,19 @@ async function handleOrganizationCreation({
}: {
organizationOnboarding: OrganizationOnboardingArg;
owner: OrgOwner;
- paymentSubscriptionId: string;
- paymentSubscriptionItemId: string;
+ paymentSubscriptionId?: string;
+ paymentSubscriptionItemId?: string;
}) {
+ if (IS_SELF_HOSTED) {
+ const deploymentRepo = new DeploymentRepository(prisma);
+ const licenseKeyService = await LicenseKeySingleton.getInstance(deploymentRepo);
+ const hasValidLicense = await licenseKeyService.checkLicense();
+
+ if (!hasValidLicense) {
+ throw new Error("Self hosted license not valid");
+ }
+ }
+
let organization;
const orgData = {
id: organizationOnboarding.organizationId,
@@ -468,8 +485,8 @@ export const createOrganizationFromOnboarding = async ({
paymentSubscriptionItemId,
}: {
organizationOnboarding: OrganizationOnboardingArg;
- paymentSubscriptionId: string;
- paymentSubscriptionItemId: string;
+ paymentSubscriptionId?: string;
+ paymentSubscriptionItemId?: string;
}) => {
log.info(
"createOrganizationFromOnboarding",
@@ -478,6 +495,11 @@ export const createOrganizationFromOnboarding = async ({
orgSlug: organizationOnboarding.slug,
})
);
+
+ if (!IS_SELF_HOSTED && (!paymentSubscriptionId || !paymentSubscriptionItemId)) {
+ throw new Error("payment_subscription_id_and_payment_subscription_item_id_are_required");
+ }
+
if (
await hasConflictingOrganization({
slug: organizationOnboarding.slug,
@@ -491,10 +513,13 @@ export const createOrganizationFromOnboarding = async ({
const userFromEmail = await findUserToBeOrgOwner(organizationOnboarding.orgOwnerEmail);
const orgOwnerTranslation = await getTranslation(userFromEmail?.locale || "en", "common");
- await handleDomainSetup({
- organizationOnboarding,
- orgOwnerTranslation,
- });
+ // TODO: We need to send emails to admin in the case of SELF_HOSTING where NEXT_PUBLIC_SINGLE_ORG_SLUG isn't set
+ if (!process.env.NEXT_PUBLIC_SINGLE_ORG_SLUG) {
+ await handleDomainSetup({
+ organizationOnboarding,
+ orgOwnerTranslation,
+ });
+ }
const { organization, owner } = await handleOrganizationCreation({
organizationOnboarding,
diff --git a/packages/features/ee/organizations/types/schemas.ts b/packages/features/ee/organizations/types/schemas.ts
new file mode 100644
index 0000000000..27c0155a20
--- /dev/null
+++ b/packages/features/ee/organizations/types/schemas.ts
@@ -0,0 +1,13 @@
+import z from "zod";
+
+import { orgOnboardingInvitedMembersSchema, orgOnboardingTeamsSchema } from "@calcom/prisma/zod-utils";
+
+// Base user schema - fields that any user can set
+export const createOrganizationSchema = z.object({
+ language: z.string().optional(),
+ logo: z.string().nullish(),
+ bio: z.string().nullish(),
+ onboardingId: z.string(),
+ invitedMembers: orgOnboardingInvitedMembersSchema.optional(),
+ teams: orgOnboardingTeamsSchema.optional(),
+});
diff --git a/packages/lib/server/repository/organizationOnboarding.ts b/packages/lib/server/repository/organizationOnboarding.ts
index 9a5c7de8ba..59a6598d79 100644
--- a/packages/lib/server/repository/organizationOnboarding.ts
+++ b/packages/lib/server/repository/organizationOnboarding.ts
@@ -23,6 +23,7 @@ export type CreateOrganizationOnboardingInput = {
teams?: { id: number; name: string; isBeingMigrated: boolean; slug: string | null }[];
error?: string | null;
isDomainConfigured?: boolean;
+ isComplete?: boolean;
};
export class OrganizationOnboardingRepository {
@@ -95,13 +96,16 @@ export class OrganizationOnboardingRepository {
static async update(id: OnboardingId, data: Partial) {
logger.debug("Updating organization onboarding", safeStringify({ id, data }));
+ // We don't want to update the createdById field in update
+ const { organizationId, createdById: _, ...rest } = data;
return await prisma.organizationOnboarding.update({
where: {
id,
},
data: {
- ...data,
+ ...rest,
+ ...(organizationId ? { organization: { connect: { id: organizationId } } } : {}),
updatedAt: new Date(),
},
});
diff --git a/packages/trpc/server/routers/viewer/organizations/_router.tsx b/packages/trpc/server/routers/viewer/organizations/_router.tsx
index ff935e97c1..2b366a6cd4 100644
--- a/packages/trpc/server/routers/viewer/organizations/_router.tsx
+++ b/packages/trpc/server/routers/viewer/organizations/_router.tsx
@@ -15,6 +15,7 @@ import { ZAdminUpdate } from "./adminUpdate.schema";
import { ZAdminVerifyInput } from "./adminVerify.schema";
import { ZBulkUsersDelete } from "./bulkDeleteUsers.schema.";
import { ZCreateInputSchema } from "./create.schema";
+import { ZCreateSelfHostedInputSchema } from "./createSelfHosted.schema";
import { ZCreateTeamsSchema } from "./createTeams.schema";
import { ZCreateWithPaymentIntentInputSchema } from "./createWithPaymentIntent.schema";
import { ZDeleteTeamInputSchema } from "./deleteTeam.schema";
@@ -158,4 +159,8 @@ export const viewerOrganizationsRouter = router({
const { default: handler } = await import("./createPhoneCall.handler");
return handler(opts);
}),
+ createSelfHosted: authedProcedure.input(ZCreateSelfHostedInputSchema).mutation(async (opts) => {
+ const { default: handler } = await import("./createSelfHosted.handler");
+ return handler(opts);
+ }),
});
diff --git a/packages/trpc/server/routers/viewer/organizations/createSelfHosted.handler.ts b/packages/trpc/server/routers/viewer/organizations/createSelfHosted.handler.ts
new file mode 100644
index 0000000000..5374315d9f
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/organizations/createSelfHosted.handler.ts
@@ -0,0 +1,75 @@
+import { createOrganizationFromOnboarding } from "@calcom/features/ee/organizations/lib/server/createOrganizationFromOnboarding";
+import { IS_SELF_HOSTED } from "@calcom/lib/constants";
+import { OrganizationOnboardingRepository } from "@calcom/lib/server/repository/organizationOnboarding";
+
+import { TRPCError } from "@trpc/server";
+
+import type { TrpcSessionUser } from "../../../types";
+import type { TCreateSelfHostedInputSchema } from "./createSelfHosted.schema";
+
+type CreateSelfHostedOptions = {
+ ctx: {
+ user: NonNullable;
+ };
+ input: TCreateSelfHostedInputSchema;
+};
+
+export const createSelfHostedHandler = async ({ input, ctx }: CreateSelfHostedOptions) => {
+ if (!IS_SELF_HOSTED) {
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: "Self-hosted is not enabled",
+ });
+ }
+
+ if (!input.onboardingId) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "onboardingId is required",
+ });
+ }
+
+ const organizationOnboarding = await OrganizationOnboardingRepository.findById(input.onboardingId);
+
+ if (!organizationOnboarding) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Organization onboarding not found",
+ });
+ }
+
+ if (!organizationOnboarding.orgOwnerEmail) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Organization owner email not found",
+ });
+ }
+
+ const { organization } = await createOrganizationFromOnboarding({
+ organizationOnboarding: {
+ id: input.onboardingId,
+ logo: input.logo ?? null,
+ bio: input.bio ?? null,
+ invitedMembers: input.invitedMembers ?? [],
+ teams: input.teams ?? [],
+ orgOwnerEmail: organizationOnboarding.orgOwnerEmail,
+ slug: organizationOnboarding.slug,
+ name: organizationOnboarding.name,
+ billingPeriod: organizationOnboarding.billingPeriod,
+ seats: organizationOnboarding.seats,
+ pricePerSeat: organizationOnboarding.pricePerSeat,
+ stripeCustomerId: organizationOnboarding.stripeCustomerId,
+ isPlatform: organizationOnboarding.isPlatform,
+ isDomainConfigured: organizationOnboarding.isDomainConfigured,
+ organizationId: organizationOnboarding.organizationId,
+ },
+ });
+
+ await OrganizationOnboardingRepository.markAsComplete(organizationOnboarding.id);
+
+ return {
+ organization,
+ };
+};
+
+export default createSelfHostedHandler;
diff --git a/packages/trpc/server/routers/viewer/organizations/createSelfHosted.schema.ts b/packages/trpc/server/routers/viewer/organizations/createSelfHosted.schema.ts
new file mode 100644
index 0000000000..5c49160fdb
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/organizations/createSelfHosted.schema.ts
@@ -0,0 +1,7 @@
+import type { z } from "zod";
+
+import { createOrganizationSchema } from "@calcom/features/ee/organizations/types/schemas";
+
+export const ZCreateSelfHostedInputSchema = createOrganizationSchema;
+
+export type TCreateSelfHostedInputSchema = z.infer;
diff --git a/packages/trpc/server/routers/viewer/organizations/createWithPaymentIntent.schema.ts b/packages/trpc/server/routers/viewer/organizations/createWithPaymentIntent.schema.ts
index 039d9d912f..b3c6747236 100644
--- a/packages/trpc/server/routers/viewer/organizations/createWithPaymentIntent.schema.ts
+++ b/packages/trpc/server/routers/viewer/organizations/createWithPaymentIntent.schema.ts
@@ -1,20 +1,12 @@
-import { z } from "zod";
+import type { z } from "zod";
-import { orgOnboardingInvitedMembersSchema, orgOnboardingTeamsSchema } from "@calcom/prisma/zod-utils";
+import { createOrganizationSchema } from "@calcom/features/ee/organizations/types/schemas";
export enum BillingPeriod {
MONTHLY = "MONTHLY",
ANNUALLY = "ANNUALLY",
}
-// Base user schema - fields that any user can set
-export const ZCreateWithPaymentIntentInputSchema = z.object({
- language: z.string().optional(),
- logo: z.string().nullish(),
- bio: z.string().nullish(),
- onboardingId: z.string(),
- invitedMembers: orgOnboardingInvitedMembersSchema.optional(),
- teams: orgOnboardingTeamsSchema.optional(),
-});
+export const ZCreateWithPaymentIntentInputSchema = createOrganizationSchema;
export type TCreateWithPaymentIntentInputSchema = z.infer;
diff --git a/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts b/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts
index 3d29a9bb6c..e7753efa26 100644
--- a/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts
+++ b/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.test.ts
@@ -2,12 +2,19 @@ import prismock from "../../../../../../tests/libs/__mocks__/prisma";
import { describe, expect, it, vi, beforeEach } from "vitest";
+import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import { intentToCreateOrgHandler } from "./intentToCreateOrg.handler";
+vi.mock("@calcom/ee/common/server/LicenseKeyService", () => ({
+ LicenseKeySingleton: {
+ getInstance: vi.fn(),
+ },
+}));
+
const mockInput = {
name: "Test Org",
slug: "test-org",
@@ -46,170 +53,266 @@ describe("intentToCreateOrgHandler", () => {
await prismock.reset();
});
- it("should allow admin user to create org for another user", async () => {
- // Create admin user
- const adminUser = await createTestUser({
- email: "admin@example.com",
- role: UserPermissionRole.ADMIN,
+ describe("hosted", () => {
+ beforeEach(() => {
+ vi.mock("@calcom/lib/constants", async () => {
+ const actualImport = await vi.importActual("@calcom/lib/constants");
+ return {
+ ...actualImport,
+ IS_SELF_HOSTED: false,
+ };
+ });
+ vi.mocked(LicenseKeySingleton.getInstance).mockResolvedValue({
+ checkLicense: vi.fn().mockResolvedValue(true),
+ });
});
- // Create target org owner with completed onboarding and verified email
- await createTestUser({
- email: mockInput.orgOwnerEmail,
- completedOnboarding: true,
- emailVerified: new Date(),
- });
+ it("should allow admin user to create org for another user", async () => {
+ // Create admin user
+ const adminUser = await createTestUser({
+ email: "admin@example.com",
+ role: UserPermissionRole.ADMIN,
+ });
- const result = await intentToCreateOrgHandler({
- input: mockInput,
- ctx: {
- user: adminUser,
- },
- });
+ // Create target org owner with completed onboarding and verified email
+ await createTestUser({
+ email: mockInput.orgOwnerEmail,
+ completedOnboarding: true,
+ emailVerified: new Date(),
+ });
- // Verify the result shape
- expect(result).toEqual({
- userId: expect.any(Number),
- orgOwnerEmail: mockInput.orgOwnerEmail,
- name: mockInput.name,
- slug: mockInput.slug,
- seats: mockInput.seats,
- pricePerSeat: mockInput.pricePerSeat,
- billingPeriod: mockInput.billingPeriod,
- isPlatform: mockInput.isPlatform,
- organizationOnboardingId: expect.any(String),
- });
-
- // Verify organization onboarding was created
- const organizationOnboarding = await prismock.organizationOnboarding.findFirst({
- where: {
- slug: mockInput.slug,
- },
- });
-
- expect(organizationOnboarding).toBeDefined();
- expect(organizationOnboarding?.name).toBe(mockInput.name);
- expect(organizationOnboarding?.slug).toBe(mockInput.slug);
- expect(organizationOnboarding?.orgOwnerEmail).toBe(mockInput.orgOwnerEmail);
- });
-
- it("should allow user to create org for themselves", async () => {
- // Create user
- const user = await createTestUser({
- email: mockInput.orgOwnerEmail,
- completedOnboarding: true,
- emailVerified: new Date(),
- });
-
- const result = await intentToCreateOrgHandler({
- input: {
- ...mockInput,
- // Use default payment settings
- seats: undefined,
- pricePerSeat: undefined,
- },
- ctx: {
- user,
- },
- });
-
- expect(result.userId).toBe(user.id);
- expect(result.orgOwnerEmail).toBe(user.email);
- });
-
- it("should throw unauthorized error when no user is logged in", async () => {
- await expect(
- intentToCreateOrgHandler({
- input: mockInput,
- ctx: {
- user: null as any,
- },
- })
- ).rejects.toThrow(new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized." }));
- });
-
- it("should throw forbidden error when non-admin tries to create org for another user", async () => {
- // Create non-admin user
- const nonAdminUser = await createTestUser({
- email: "nonadmin@example.com",
- role: UserPermissionRole.USER,
- });
-
- await expect(
- intentToCreateOrgHandler({
- input: mockInput,
- ctx: {
- user: nonAdminUser,
- },
- })
- ).rejects.toThrow(
- new TRPCError({
- code: "FORBIDDEN",
- message: "You can only create organization where you are the owner",
- })
- );
- });
-
- it("should throw error when target user is not found", async () => {
- // Create admin user
- const adminUser = await createTestUser({
- email: "admin@example.com",
- role: UserPermissionRole.ADMIN,
- });
-
- await expect(
- intentToCreateOrgHandler({
+ const result = await intentToCreateOrgHandler({
input: mockInput,
ctx: {
user: adminUser,
},
- })
- ).rejects.toThrow(
- new TRPCError({
- code: "BAD_REQUEST",
- message: `No user found with email ${mockInput.orgOwnerEmail}`,
- })
- );
- });
+ });
- it("should throw error when organization onboarding already exists", async () => {
- // Create admin user
- const adminUser = await createTestUser({
- email: "admin@example.com",
- role: UserPermissionRole.ADMIN,
- });
-
- // Create target org owner
- await createTestUser({
- email: mockInput.orgOwnerEmail,
- completedOnboarding: true,
- emailVerified: new Date(),
- });
-
- // Create existing organization onboarding
- await prismock.organizationOnboarding.create({
- data: {
+ // Verify the result shape
+ expect(result).toEqual({
+ userId: expect.any(Number),
+ orgOwnerEmail: mockInput.orgOwnerEmail,
name: mockInput.name,
slug: mockInput.slug,
- orgOwnerEmail: mockInput.orgOwnerEmail,
- billingPeriod: mockInput.billingPeriod,
seats: mockInput.seats,
pricePerSeat: mockInput.pricePerSeat,
- createdById: adminUser.id,
- },
+ billingPeriod: mockInput.billingPeriod,
+ isPlatform: mockInput.isPlatform,
+ organizationOnboardingId: expect.any(String),
+ });
+
+ // Verify organization onboarding was created
+ const organizationOnboarding = await prismock.organizationOnboarding.findFirst({
+ where: {
+ slug: mockInput.slug,
+ },
+ });
+
+ expect(organizationOnboarding).toBeDefined();
+ expect(organizationOnboarding?.name).toBe(mockInput.name);
+ expect(organizationOnboarding?.slug).toBe(mockInput.slug);
+ expect(organizationOnboarding?.orgOwnerEmail).toBe(mockInput.orgOwnerEmail);
});
- await expect(
- intentToCreateOrgHandler({
- input: mockInput,
- ctx: {
- user: adminUser,
+ it("should allow user to create org for themselves", async () => {
+ // Create user
+ const user = await createTestUser({
+ email: mockInput.orgOwnerEmail,
+ completedOnboarding: true,
+ emailVerified: new Date(),
+ });
+
+ const result = await intentToCreateOrgHandler({
+ input: {
+ ...mockInput,
+ // Use default payment settings
+ seats: undefined,
+ pricePerSeat: undefined,
},
- })
- ).rejects.toThrow("organization_onboarding_already_exists");
+ ctx: {
+ user,
+ },
+ });
+
+ expect(result.userId).toBe(user.id);
+ expect(result.orgOwnerEmail).toBe(user.email);
+ });
+
+ it("should throw unauthorized error when no user is logged in", async () => {
+ await expect(
+ intentToCreateOrgHandler({
+ input: mockInput,
+ ctx: {
+ user: null as any,
+ },
+ })
+ ).rejects.toThrow(new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized." }));
+ });
+
+ it("should throw forbidden error when non-admin tries to create org for another user", async () => {
+ // Create non-admin user
+ const nonAdminUser = await createTestUser({
+ email: "nonadmin@example.com",
+ role: UserPermissionRole.USER,
+ });
+
+ await expect(
+ intentToCreateOrgHandler({
+ input: mockInput,
+ ctx: {
+ user: nonAdminUser,
+ },
+ })
+ ).rejects.toThrow(
+ new TRPCError({
+ code: "FORBIDDEN",
+ message: "You can only create organization where you are the owner",
+ })
+ );
+ });
+
+ it("should throw error when target user is not found", async () => {
+ // Create admin user
+ const adminUser = await createTestUser({
+ email: "admin@example.com",
+ role: UserPermissionRole.ADMIN,
+ });
+
+ await expect(
+ intentToCreateOrgHandler({
+ input: mockInput,
+ ctx: {
+ user: adminUser,
+ },
+ })
+ ).rejects.toThrow(
+ new TRPCError({
+ code: "BAD_REQUEST",
+ message: `No user found with email ${mockInput.orgOwnerEmail}`,
+ })
+ );
+ });
+
+ it("should throw error when organization onboarding already exists", async () => {
+ // Create admin user
+ const adminUser = await createTestUser({
+ email: "admin@example.com",
+ role: UserPermissionRole.ADMIN,
+ });
+
+ // Create target org owner
+ await createTestUser({
+ email: mockInput.orgOwnerEmail,
+ completedOnboarding: true,
+ emailVerified: new Date(),
+ });
+
+ // Create existing organization onboarding
+ await prismock.organizationOnboarding.create({
+ data: {
+ name: mockInput.name,
+ slug: mockInput.slug,
+ orgOwnerEmail: mockInput.orgOwnerEmail,
+ billingPeriod: mockInput.billingPeriod,
+ seats: mockInput.seats,
+ pricePerSeat: mockInput.pricePerSeat,
+ createdById: adminUser.id,
+ },
+ });
+
+ await expect(
+ intentToCreateOrgHandler({
+ input: mockInput,
+ ctx: {
+ user: adminUser,
+ },
+ })
+ ).rejects.toThrow("organization_onboarding_already_exists");
+ });
});
// This endpoint doesn't handle platform organizations as that is completely different flow at the moment
// eslint-disable-next-line playwright/no-skipped-test
describe.skip("platform organization tests");
+
+ describe("self hosted", () => {
+ beforeEach(() => {
+ vi.mock("@calcom/lib/constants", async () => {
+ const actualImport = await vi.importActual("@calcom/lib/constants");
+ return {
+ ...actualImport,
+ IS_SELF_HOSTED: true,
+ };
+ });
+ });
+
+ it("should throw error when license is not valid", async () => {
+ vi.mocked(LicenseKeySingleton.getInstance).mockResolvedValue({
+ checkLicense: vi.fn().mockResolvedValue(false),
+ });
+ const adminUser = await createTestUser({
+ email: "admin@example.com",
+ role: UserPermissionRole.ADMIN,
+ });
+
+ await expect(
+ intentToCreateOrgHandler({
+ input: mockInput,
+ ctx: {
+ user: adminUser,
+ },
+ })
+ ).rejects.toThrow("License is not valid");
+ });
+
+ it("should allow admin user to create org for another user", async () => {
+ vi.mocked(LicenseKeySingleton.getInstance).mockResolvedValue({
+ checkLicense: vi.fn().mockResolvedValue(true),
+ });
+ // Create admin user
+ const adminUser = await createTestUser({
+ email: "admin@example.com",
+ role: UserPermissionRole.ADMIN,
+ });
+
+ // Create target org owner with completed onboarding and verified email
+ await createTestUser({
+ email: mockInput.orgOwnerEmail,
+ completedOnboarding: true,
+ emailVerified: new Date(),
+ });
+
+ const result = await intentToCreateOrgHandler({
+ input: mockInput,
+ ctx: {
+ user: adminUser,
+ },
+ });
+
+ // Verify the result shape
+ expect(result).toEqual({
+ userId: expect.any(Number),
+ orgOwnerEmail: mockInput.orgOwnerEmail,
+ name: mockInput.name,
+ slug: mockInput.slug,
+ seats: mockInput.seats,
+ pricePerSeat: mockInput.pricePerSeat,
+ billingPeriod: mockInput.billingPeriod,
+ isPlatform: mockInput.isPlatform,
+ organizationOnboardingId: expect.any(String),
+ });
+
+ // Verify organization onboarding was created
+ const organizationOnboarding = await prismock.organizationOnboarding.findFirst({
+ where: {
+ slug: mockInput.slug,
+ },
+ });
+
+ expect(organizationOnboarding).toBeDefined();
+ expect(organizationOnboarding?.name).toBe(mockInput.name);
+ expect(organizationOnboarding?.slug).toBe(mockInput.slug);
+ expect(organizationOnboarding?.orgOwnerEmail).toBe(mockInput.orgOwnerEmail);
+ });
+ });
});
diff --git a/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.ts b/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.ts
index 5437a0978a..b61500d926 100644
--- a/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.ts
+++ b/packages/trpc/server/routers/viewer/organizations/intentToCreateOrg.handler.ts
@@ -1,11 +1,15 @@
+import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService";
import { OrganizationPaymentService } from "@calcom/features/ee/organizations/lib/OrganizationPaymentService";
import {
assertCanCreateOrg,
findUserToBeOrgOwner,
} from "@calcom/features/ee/organizations/lib/server/orgCreationUtils";
+import { IS_SELF_HOSTED } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
+import { DeploymentRepository } from "@calcom/lib/server/repository/deployment";
import { OrganizationOnboardingRepository } from "@calcom/lib/server/repository/organizationOnboarding";
+import { prisma } from "@calcom/prisma";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
@@ -29,6 +33,20 @@ export const intentToCreateOrgHandler = async ({ input, ctx }: CreateOptions) =>
safeStringify({ slug, name, orgOwnerEmail, isPlatform })
);
+ if (IS_SELF_HOSTED) {
+ const deploymentRepo = new DeploymentRepository(prisma);
+ const licenseKeyService = await LicenseKeySingleton.getInstance(deploymentRepo);
+ const hasValidLicense = await licenseKeyService.checkLicense();
+
+ if (!hasValidLicense) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ // TODO: We need to send translation keys from here and frontend should translate it
+ message: "License is not valid",
+ });
+ }
+ }
+
const loggedInUser = ctx.user;
if (!loggedInUser) throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized." });