* refactor: replace FeaturesRepository with DI-based feature repositories Migrate from the legacy FeaturesRepository pattern to separate DI-based repositories: - Add checkIfFeatureIsEnabledGlobally to IFeatureRepository - Add getTeamsWithFeatureEnabled to ITeamFeatureRepository - Update CalendarSubscriptionService to use featureRepository, teamFeatureRepository, and userFeatureRepository - Update SelectedCalendarRepository.findNextSubscriptionBatch to filter by teamIds instead of featureIds - Update OnboardingPathService to use DI via getFeatureRepository() - Remove prisma parameter from OnboardingPathService callsites Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test: update tests to use new DI-based feature repositories Update CalendarSubscriptionService and SelectedCalendarRepository tests to use the new separate repository interfaces: - featureRepository for global feature checks - teamFeatureRepository for team-level feature checks - userFeatureRepository for user-level feature checks Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: update API routes to use DI-based feature repositories Update cron and webhook routes to use the new separate repository interfaces instead of the combined FeaturesRepository. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: remove prisma arg from getGettingStartedPathWithParams call The OnboardingPathService method no longer requires a prisma argument as it now uses DI containers internally. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test: update route tests to expect new DI-based feature repositories Update service instantiation tests to expect featureRepository, teamFeatureRepository, and userFeatureRepository instead of the old featuresRepository. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: optimize global feature check and add guard in checkForNewSubscriptions - Use targeted select for only `enabled` field in checkIfFeatureIsEnabledGlobally - Add global feature flag guard in checkForNewSubscriptions to avoid unnecessary DB queries and API calls when the cache feature is globally disabled Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * remove redundant comment --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
158 lines
4.8 KiB
TypeScript
158 lines
4.8 KiB
TypeScript
import process from "node:process";
|
|
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
|
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
|
import stripe from "@calcom/features/ee/payments/server/stripe";
|
|
import { hostedCal, isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
|
|
import { ssoTenantProduct } from "@calcom/features/ee/sso/lib/sso";
|
|
import { OnboardingPathService } from "@calcom/features/onboarding/lib/onboarding-path.service";
|
|
import { checkUsername } from "@calcom/features/profile/lib/checkUsername";
|
|
import { IS_PREMIUM_USERNAME_ENABLED } from "@calcom/lib/constants";
|
|
import { getTrackingFromCookies, type TrackingData } from "@calcom/lib/tracking";
|
|
import { prisma } from "@calcom/prisma";
|
|
import type { GetServerSidePropsContext } from "next";
|
|
import { z } from "zod";
|
|
|
|
const Params = z.object({
|
|
username: z.string().optional(),
|
|
email: z.string().optional(),
|
|
provider: z.string({ required_error: "File is not named sso/[provider]" }),
|
|
});
|
|
|
|
export const getServerSideProps = async ({ req, query }: GetServerSidePropsContext) => {
|
|
const { provider: providerParam, email: emailParam, username: usernameParam } = Params.parse(query);
|
|
|
|
const successDestination = await OnboardingPathService.getGettingStartedPathWithParams(
|
|
usernameParam ? { username: usernameParam } : undefined
|
|
);
|
|
|
|
const session = await getServerSession({ req });
|
|
|
|
const { currentOrgDomain } = orgDomainConfig(req);
|
|
|
|
if (session) {
|
|
// Validating if username is Premium, while this is true an email its required for stripe user confirmation
|
|
if (usernameParam && session.user.email) {
|
|
const availability = await checkUsername(usernameParam, currentOrgDomain);
|
|
if (availability.available && availability.premium && IS_PREMIUM_USERNAME_ENABLED) {
|
|
const tracking = getTrackingFromCookies(req.cookies);
|
|
const stripePremiumUrl = await getStripePremiumUsernameUrl({
|
|
userId: session.user.id.toString(),
|
|
userEmail: session.user.email,
|
|
username: usernameParam,
|
|
successDestination,
|
|
tracking,
|
|
});
|
|
if (stripePremiumUrl) {
|
|
return {
|
|
redirect: {
|
|
destination: stripePremiumUrl,
|
|
permanent: false,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
redirect: {
|
|
destination: successDestination,
|
|
permanent: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
let error: string | null = null;
|
|
|
|
let tenant = samlTenantID;
|
|
let product = samlProductID;
|
|
|
|
if (providerParam === "saml" && hostedCal) {
|
|
if (!emailParam) {
|
|
error = "Email not provided";
|
|
} else {
|
|
try {
|
|
const ret = await ssoTenantProduct(prisma, emailParam);
|
|
tenant = ret.tenant;
|
|
product = ret.product;
|
|
} catch (e) {
|
|
if (e instanceof Error) {
|
|
error = e.message;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (error) {
|
|
return {
|
|
redirect: {
|
|
destination: `/auth/error?error=${error}`,
|
|
permanent: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
props: {
|
|
provider: providerParam,
|
|
isSAMLLoginEnabled,
|
|
hostedCal,
|
|
tenant,
|
|
product,
|
|
error,
|
|
},
|
|
};
|
|
};
|
|
|
|
type GetStripePremiumUsernameUrl = {
|
|
userId: string;
|
|
userEmail: string;
|
|
username: string;
|
|
successDestination: string;
|
|
tracking?: TrackingData;
|
|
};
|
|
|
|
const getStripePremiumUsernameUrl = async ({
|
|
userId,
|
|
userEmail,
|
|
username,
|
|
successDestination,
|
|
tracking,
|
|
}: GetStripePremiumUsernameUrl): Promise<string | null> => {
|
|
// @TODO: probably want to check if stripe user email already exists? or not
|
|
const customer = await stripe.customers.create({
|
|
email: userEmail,
|
|
metadata: {
|
|
email: userEmail,
|
|
username,
|
|
},
|
|
});
|
|
|
|
const checkoutSession = await stripe.checkout.sessions.create({
|
|
mode: "subscription",
|
|
customer: customer.id,
|
|
line_items: [
|
|
{
|
|
price: getPremiumMonthlyPlanPriceId(),
|
|
quantity: 1,
|
|
},
|
|
],
|
|
success_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}${successDestination}&session_id={CHECKOUT_SESSION_ID}`,
|
|
cancel_url: process.env.NEXT_PUBLIC_WEBAPP_URL || "https://app.cal.com",
|
|
allow_promotion_codes: true,
|
|
metadata: {
|
|
dubCustomerId: userId, // pass the userId during checkout creation for sales conversion tracking: https://d.to/conversions/stripe
|
|
...(tracking?.googleAds?.gclid && {
|
|
gclid: tracking.googleAds.gclid,
|
|
campaignId: tracking.googleAds.campaignId,
|
|
}),
|
|
...(tracking?.linkedInAds?.liFatId && {
|
|
liFatId: tracking.linkedInAds.liFatId,
|
|
linkedInCampaignId: tracking.linkedInAds?.campaignId,
|
|
}),
|
|
},
|
|
});
|
|
|
|
return checkoutSession.url;
|
|
};
|