Files
Eunjae LeeandClaude Opus 4.5 512002aaef refactor: replace FeaturesRepository with DI-based feature repositories (#27200)
* 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>
2026-02-06 09:34:03 +00:00

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;
};