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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* remove redundant comment

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
This commit is contained in:
Eunjae Lee
2026-02-06 09:34:03 +00:00
committed by GitHub
co-authored by Claude Opus 4.5
parent cb36fc201f
commit 512002aaef
21 changed files with 265 additions and 285 deletions
@@ -1,13 +1,11 @@
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { isCompanyEmail } from "@calcom/features/ee/organizations/lib/utils";
import { OnboardingPathService } from "@calcom/features/onboarding/lib/onboarding-path.service";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { prisma } from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
export default async function OrganizationOnboardingLayout({ children }: { children: React.ReactNode }) {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
@@ -19,7 +17,7 @@ export default async function OrganizationOnboardingLayout({ children }: { child
const userEmail = session.user.email || "";
const userId = session.user.id;
const gettingStartedPath = await OnboardingPathService.getGettingStartedPath(prisma);
const gettingStartedPath = await OnboardingPathService.getGettingStartedPath();
if (!isCompanyEmail(userEmail)) {
return redirect(gettingStartedPath);
@@ -1,14 +1,11 @@
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { isCompanyEmail } from "@calcom/features/ee/organizations/lib/utils";
import { OnboardingPathService } from "@calcom/features/onboarding/lib/onboarding-path.service";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { prisma } from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { OrganizationMigrateMembersView } from "~/onboarding/organization/migrate-members/organization-migrate-members-view";
export default async function MigrateMembersPage() {
@@ -21,14 +18,16 @@ export default async function MigrateMembersPage() {
const userEmail = session.user.email || "";
const userId = session.user.id;
const gettingStartedPath = await OnboardingPathService.getGettingStartedPath(prisma);
const gettingStartedPath = await OnboardingPathService.getGettingStartedPath();
if (!isCompanyEmail(userEmail)) {
return redirect(gettingStartedPath);
}
const userRepository = new UserRepository(prisma);
const isMemberOfOrganization = await userRepository.findIfAMemberOfSomeOrganization({ user: { id: userId } });
const isMemberOfOrganization = await userRepository.findIfAMemberOfSomeOrganization({
user: { id: userId },
});
if (isMemberOfOrganization) {
return redirect(gettingStartedPath);
@@ -1,15 +1,12 @@
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { isCompanyEmail } from "@calcom/features/ee/organizations/lib/utils";
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
import { OnboardingPathService } from "@calcom/features/onboarding/lib/onboarding-path.service";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { prisma } from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { OrganizationMigrateTeamsView } from "~/onboarding/organization/migrate-teams/organization-migrate-teams-view";
type PageProps = {
@@ -26,7 +23,7 @@ export default async function MigrateTeamsPage({ searchParams }: PageProps) {
const userEmail = session.user.email || "";
const userId = session.user.id;
const gettingStartedPath = await OnboardingPathService.getGettingStartedPath(prisma);
const gettingStartedPath = await OnboardingPathService.getGettingStartedPath();
if (!isCompanyEmail(userEmail)) {
return redirect(gettingStartedPath);
@@ -220,7 +220,9 @@ describe("/api/cron/calendar-subscriptions", () => {
expect(mockCalendarSubscriptionService).toHaveBeenCalledWith({
adapterFactory: expect.any(Object),
selectedCalendarRepository: expect.any(Object),
featuresRepository: expect.any(Object),
featureRepository: expect.any(Object),
teamFeatureRepository: expect.any(Object),
userFeatureRepository: expect.any(Object),
calendarSyncService: expect.any(Object),
calendarCacheEventService: expect.any(Object),
});
@@ -1,16 +1,18 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import process from "node:process";
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
import { DefaultAdapterFactory } from "@calcom/features/calendar-subscription/adapters/AdaptersFactory";
import { CalendarSubscriptionService } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionService";
import { CalendarCacheEventRepository } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository";
import { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
import { CalendarSyncService } from "@calcom/features/calendar-subscription/lib/sync/CalendarSyncService";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { getFeatureRepository } from "@calcom/features/di/containers/FeatureRepository";
import { getTeamFeatureRepository } from "@calcom/features/di/containers/TeamFeatureRepository";
import { getUserFeatureRepository } from "@calcom/features/di/containers/UserFeatureRepository";
import { SelectedCalendarRepository } from "@calcom/features/selectedCalendar/repositories/SelectedCalendarRepository";
import { prisma } from "@calcom/prisma";
import { defaultResponderForAppDir } from "@calcom/web/app/api/defaultResponderForAppDir";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
/**
* Cron webhook
@@ -39,7 +41,9 @@ async function getHandler(request: NextRequest) {
const calendarSubscriptionService = new CalendarSubscriptionService({
adapterFactory: new DefaultAdapterFactory(),
selectedCalendarRepository: new SelectedCalendarRepository(prisma),
featuresRepository: new FeaturesRepository(prisma),
featureRepository: getFeatureRepository(),
teamFeatureRepository: getTeamFeatureRepository(),
userFeatureRepository: getUserFeatureRepository(),
calendarSyncService,
calendarCacheEventService,
});
@@ -306,7 +306,9 @@ describe("/api/webhooks/calendar-subscription/[provider]", () => {
expect(mockCalendarSubscriptionService).toHaveBeenCalledWith({
adapterFactory: expect.any(Object),
selectedCalendarRepository: expect.any(Object),
featuresRepository: expect.any(Object),
featureRepository: expect.any(Object),
teamFeatureRepository: expect.any(Object),
userFeatureRepository: expect.any(Object),
calendarSyncService: expect.any(Object),
calendarCacheEventService: expect.any(Object),
});
@@ -1,7 +1,3 @@
import type { Params } from "app/_types";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
import type { CalendarSubscriptionProvider } from "@calcom/features/calendar-subscription/adapters/AdaptersFactory";
import { DefaultAdapterFactory } from "@calcom/features/calendar-subscription/adapters/AdaptersFactory";
@@ -9,11 +5,16 @@ import { CalendarSubscriptionService } from "@calcom/features/calendar-subscript
import { CalendarCacheEventRepository } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository";
import { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
import { CalendarSyncService } from "@calcom/features/calendar-subscription/lib/sync/CalendarSyncService";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import logger from "@calcom/lib/logger";
import { getFeatureRepository } from "@calcom/features/di/containers/FeatureRepository";
import { getTeamFeatureRepository } from "@calcom/features/di/containers/TeamFeatureRepository";
import { getUserFeatureRepository } from "@calcom/features/di/containers/UserFeatureRepository";
import { SelectedCalendarRepository } from "@calcom/features/selectedCalendar/repositories/SelectedCalendarRepository";
import logger from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
import { defaultResponderForAppDir } from "@calcom/web/app/api/defaultResponderForAppDir";
import type { Params } from "app/_types";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
const log = logger.getSubLogger({ prefix: ["calendar-webhook"] });
@@ -59,7 +60,9 @@ async function postHandler(request: NextRequest, ctx: { params: Promise<Params>
const calendarSubscriptionService = new CalendarSubscriptionService({
adapterFactory: new DefaultAdapterFactory(),
selectedCalendarRepository: new SelectedCalendarRepository(prisma),
featuresRepository: new FeaturesRepository(prisma),
featureRepository: getFeatureRepository(),
teamFeatureRepository: getTeamFeatureRepository(),
userFeatureRepository: getUserFeatureRepository(),
calendarSyncService,
calendarCacheEventService,
});
+5 -8
View File
@@ -1,17 +1,14 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { getBillingProviderService } from "@calcom/features/ee/billing/di/containers/Billing";
import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container";
import { OnboardingPathService } from "@calcom/features/onboarding/lib/onboarding-path.service";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { IS_STRIPE_ENABLED } from "@calcom/lib/constants";
import { IS_STRIPE_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { CreationSource } from "@calcom/prisma/enums";
import { CreationSource, MembershipRole } from "@calcom/prisma/enums";
import { userMetadata } from "@calcom/prisma/zod-utils";
import { inviteMembersWithNoInviterPermissionCheck } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
const verifySchema = z.object({
token: z.string(),
@@ -174,7 +171,7 @@ export async function handler(req: NextApiRequest, res: NextApiResponse) {
await moveUserToMatchingOrg({ email: user.email });
const gettingStartedPath = await OnboardingPathService.getGettingStartedPath(prisma);
const gettingStartedPath = await OnboardingPathService.getGettingStartedPath();
return res.redirect(`${WEBAPP_URL}${hasCompletedOnboarding ? "/event-types" : gettingStartedPath}`);
}
@@ -1,16 +1,16 @@
import type { GetServerSidePropsContext } from "next";
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 { checkUsername } from "@calcom/features/profile/lib/checkUsername";
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({
@@ -20,15 +20,9 @@ const Params = z.object({
});
export const getServerSideProps = async ({ req, query }: GetServerSidePropsContext) => {
const {
provider: providerParam,
email: emailParam,
username: usernameParam,
} = Params.parse(query);
const { provider: providerParam, email: emailParam, username: usernameParam } = Params.parse(query);
const successDestination = await OnboardingPathService.getGettingStartedPathWithParams(
prisma,
usernameParam ? { username: usernameParam } : undefined
);
@@ -148,8 +142,14 @@ const getStripePremiumUsernameUrl = async ({
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 }),
...(tracking?.googleAds?.gclid && {
gclid: tracking.googleAds.gclid,
campaignId: tracking.googleAds.campaignId,
}),
...(tracking?.linkedInAds?.liFatId && {
liFatId: tracking.linkedInAds.liFatId,
linkedInCampaignId: tracking.linkedInAds?.campaignId,
}),
},
});
@@ -9,11 +9,13 @@ import type {
} from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionPort.interface";
import type { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
import type { CalendarSyncService } from "@calcom/features/calendar-subscription/lib/sync/CalendarSyncService";
import type { FeaturesRepository } from "@calcom/features/flags/features.repository";
import type { IFeatureRepository } from "@calcom/features/flags/repositories/PrismaFeatureRepository";
import type { ITeamFeatureRepository } from "@calcom/features/flags/repositories/PrismaTeamFeatureRepository";
import type { IUserFeatureRepository } from "@calcom/features/flags/repositories/PrismaUserFeatureRepository";
import type { ISelectedCalendarRepository } from "@calcom/features/selectedCalendar/repositories/SelectedCalendarRepository.interface";
import logger from "@calcom/lib/logger";
import { metrics } from "@sentry/nextjs";
import type { SelectedCalendar } from "@calcom/prisma/client";
import { metrics } from "@sentry/nextjs";
// biome-ignore lint/nursery/useExplicitType: logger type is inferred
const log = logger.getSubLogger({ prefix: ["CalendarSubscriptionService"] });
@@ -27,7 +29,11 @@ export class CalendarSubscriptionService {
private deps: {
adapterFactory: AdapterFactory;
selectedCalendarRepository: ISelectedCalendarRepository;
featuresRepository: FeaturesRepository;
featureRepository: IFeatureRepository;
teamFeatureRepository: ITeamFeatureRepository;
userFeatureRepository: IUserFeatureRepository & {
checkIfUserHasFeature: (userId: number, slug: string) => Promise<boolean>;
};
calendarCacheEventService: CalendarCacheEventService;
calendarSyncService: CalendarSyncService;
}
@@ -135,16 +141,12 @@ export class CalendarSubscriptionService {
* Process webhook
*/
// biome-ignore lint/complexity/noExcessiveLinesPerFunction: webhook processing requires multiple steps
async processWebhook(
provider: CalendarSubscriptionProvider,
request: Request
) {
async processWebhook(provider: CalendarSubscriptionProvider, request: Request) {
const startTime = performance.now();
log.debug("processWebhook", { provider });
try {
const calendarSubscriptionAdapter =
this.deps.adapterFactory.get(provider);
const calendarSubscriptionAdapter = this.deps.adapterFactory.get(provider);
const isValid = await calendarSubscriptionAdapter.validate(request);
if (!isValid) {
@@ -154,9 +156,7 @@ export class CalendarSubscriptionService {
throw new Error("Invalid webhook request");
}
const channelId = await calendarSubscriptionAdapter.extractChannelId(
request
);
const channelId = await calendarSubscriptionAdapter.extractChannelId(request);
if (!channelId) {
metrics.count("calendar.subscription.webhook.missing_channel", 1, {
attributes: { provider },
@@ -166,8 +166,7 @@ export class CalendarSubscriptionService {
log.debug("Processing webhook", { channelId });
const selectedCalendar =
await this.deps.selectedCalendarRepository.findByChannelId(channelId);
const selectedCalendar = await this.deps.selectedCalendarRepository.findByChannelId(channelId);
if (!selectedCalendar) {
metrics.count("calendar.subscription.webhook.skipped", 1, {
attributes: { provider, reason: "calendar_not_found" },
@@ -188,19 +187,13 @@ export class CalendarSubscriptionService {
attributes: { provider },
});
metrics.distribution(
"calendar.subscription.webhook.duration_ms",
durationMs,
{
attributes: { provider },
}
);
metrics.distribution("calendar.subscription.webhook.duration_ms", durationMs, {
attributes: { provider },
});
metrics.distribution(
"calendar.subscription.webhook.events_fetched",
eventsProcessed.eventsFetched,
{ attributes: { provider } }
);
metrics.distribution("calendar.subscription.webhook.events_fetched", eventsProcessed.eventsFetched, {
attributes: { provider },
});
} catch (error) {
const durationMs = performance.now() - startTime;
@@ -208,13 +201,9 @@ export class CalendarSubscriptionService {
attributes: { provider },
});
metrics.distribution(
"calendar.subscription.webhook.duration_ms",
durationMs,
{
attributes: { provider, outcome: "error" },
}
);
metrics.distribution("calendar.subscription.webhook.duration_ms", durationMs, {
attributes: { provider, outcome: "error" },
});
log.error("Webhook processing failed", {
provider,
@@ -236,9 +225,7 @@ export class CalendarSubscriptionService {
* @returns Object with counts of events fetched, cached, and synced, plus propagation lag metrics
*/
// biome-ignore lint/complexity/noExcessiveLinesPerFunction: event processing requires multiple steps
async processEvents(
selectedCalendar: SelectedCalendar
): Promise<{
async processEvents(selectedCalendar: SelectedCalendar): Promise<{
eventsFetched: number;
eventsCached: number;
eventsSynced: number;
@@ -261,10 +248,7 @@ export class CalendarSubscriptionService {
selectedCalendar.integration as CalendarSubscriptionProvider
);
if (
!selectedCalendar.credentialId &&
!selectedCalendar.delegationCredentialId
) {
if (!selectedCalendar.credentialId && !selectedCalendar.delegationCredentialId) {
log.debug("Selected Calendar doesn't have credentials", {
selectedCalendarId: selectedCalendar.id,
});
@@ -293,10 +277,7 @@ export class CalendarSubscriptionService {
let events: CalendarSubscriptionEvent | null = null;
try {
events = await calendarSubscriptionAdapter.fetchEvents(
selectedCalendar,
credential
);
events = await calendarSubscriptionAdapter.fetchEvents(selectedCalendar, credential);
} catch (err) {
metrics.count("calendar.subscription.events.fetch.error", 1, {
attributes: { provider: selectedCalendar.integration },
@@ -315,87 +296,59 @@ export class CalendarSubscriptionService {
result.eventsFetched = events.items.length;
metrics.distribution(
"calendar.subscription.events.fetched",
events.items.length,
{
attributes: {
provider: selectedCalendar.integration,
incremental: !!selectedCalendar.syncToken,
},
}
);
metrics.distribution("calendar.subscription.events.fetched", events.items.length, {
attributes: {
provider: selectedCalendar.integration,
incremental: !!selectedCalendar.syncToken,
},
});
const now = Date.now();
const lagStats = this.calculatePropagationLag(events.items, now);
if (lagStats) {
result.propagationLagMs = lagStats;
metrics.distribution(
"calendar.subscription.propagation_lag.avg_ms",
lagStats.avg,
{
attributes: { provider: selectedCalendar.integration },
}
);
metrics.distribution(
"calendar.subscription.propagation_lag.max_ms",
lagStats.max,
{
attributes: { provider: selectedCalendar.integration },
}
);
metrics.distribution("calendar.subscription.propagation_lag.avg_ms", lagStats.avg, {
attributes: { provider: selectedCalendar.integration },
});
metrics.distribution("calendar.subscription.propagation_lag.max_ms", lagStats.max, {
attributes: { provider: selectedCalendar.integration },
});
}
await this.deps.selectedCalendarRepository.updateSyncStatus(selectedCalendar.id, {
syncToken: events.syncToken || selectedCalendar.syncToken,
syncedAt: new Date(),
syncErrorAt: null,
syncErrorCount: 0,
}
);
syncToken: events.syncToken || selectedCalendar.syncToken,
syncedAt: new Date(),
syncErrorAt: null,
syncErrorCount: 0,
});
if (cacheEnabled && cacheEnabledForUser) {
log.debug("Caching events", { count: events.items.length });
await this.deps.calendarCacheEventService.handleEvents(selectedCalendar, events.items );
await this.deps.calendarCacheEventService.handleEvents(selectedCalendar, events.items);
result.eventsCached = events.items.length;
metrics.distribution(
"calendar.subscription.events.cached",
events.items.length,
{
attributes: { provider: selectedCalendar.integration },
}
);
metrics.distribution("calendar.subscription.events.cached", events.items.length, {
attributes: { provider: selectedCalendar.integration },
});
}
if (syncEnabled) {
log.debug("Syncing events", { count: events.items.length });
await this.deps.calendarSyncService.handleEvents(
selectedCalendar,
events.items
);
await this.deps.calendarSyncService.handleEvents(selectedCalendar, events.items);
result.eventsSynced = events.items.length;
metrics.distribution(
"calendar.subscription.events.synced",
events.items.length,
{
attributes: { provider: selectedCalendar.integration },
}
);
metrics.distribution("calendar.subscription.events.synced", events.items.length, {
attributes: { provider: selectedCalendar.integration },
});
}
metrics.distribution(
"calendar.subscription.processEvents.duration_ms",
performance.now() - startTime,
{
attributes: {
provider: selectedCalendar.integration,
cache: cacheEnabled && cacheEnabledForUser ? "on" : "off",
sync: syncEnabled ? "on" : "off",
},
}
);
metrics.distribution("calendar.subscription.processEvents.duration_ms", performance.now() - startTime, {
attributes: {
provider: selectedCalendar.integration,
cache: cacheEnabled && cacheEnabledForUser ? "on" : "off",
sync: syncEnabled ? "on" : "off",
},
});
return result;
}
@@ -440,10 +393,16 @@ export class CalendarSubscriptionService {
*/
// biome-ignore lint/nursery/useExplicitType: return type is void
async checkForNewSubscriptions() {
if (!(await this.isCacheEnabled())) {
return;
}
const teamIds = await this.deps.teamFeatureRepository.getTeamsWithFeatureEnabled(
CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE
);
const rows = await this.deps.selectedCalendarRepository.findNextSubscriptionBatch({
take: 100,
integrations: this.deps.adapterFactory.getProviders(),
featureIds: [CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE],
teamIds,
genericCalendarSuffixes: this.deps.adapterFactory.getGenericCalendarSuffixes(),
});
log.debug("checkForNewSubscriptions", { count: rows.length });
@@ -455,7 +414,7 @@ export class CalendarSubscriptionService {
* @returns true if cache is enabled
*/
async isCacheEnabled(): Promise<boolean> {
return this.deps.featuresRepository.checkIfFeatureIsEnabledGlobally(
return this.deps.featureRepository.checkIfFeatureIsEnabledGlobally(
CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE
);
}
@@ -465,7 +424,7 @@ export class CalendarSubscriptionService {
* @returns true if cache is enabled
*/
async isCacheEnabledForUser(userId: number): Promise<boolean> {
return this.deps.featuresRepository.checkIfUserHasFeature(
return this.deps.userFeatureRepository.checkIfUserHasFeature(
userId,
CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE
);
@@ -476,7 +435,7 @@ export class CalendarSubscriptionService {
* @returns true if sync is enabled
*/
async isSyncEnabled(): Promise<boolean> {
return this.deps.featuresRepository.checkIfFeatureIsEnabledGlobally(
return this.deps.featureRepository.checkIfFeatureIsEnabledGlobally(
CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_SYNC_FEATURE
);
}
@@ -1,6 +1,6 @@
import "../__mocks__/delegationCredential";
import { describe, test, expect, vi, beforeEach } from "vitest";
import { beforeEach, describe, expect, test, vi } from "vitest";
vi.mock("@sentry/nextjs", () => ({
metrics: {
@@ -12,10 +12,11 @@ vi.mock("@sentry/nextjs", () => ({
import type { AdapterFactory } from "@calcom/features/calendar-subscription/adapters/AdaptersFactory";
import type { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
import type { CalendarSyncService } from "@calcom/features/calendar-subscription/lib/sync/CalendarSyncService";
import type { FeaturesRepository } from "@calcom/features/flags/features.repository";
import type { IFeatureRepository } from "@calcom/features/flags/repositories/PrismaFeatureRepository";
import type { ITeamFeatureRepository } from "@calcom/features/flags/repositories/PrismaTeamFeatureRepository";
import type { IUserFeatureRepository } from "@calcom/features/flags/repositories/PrismaUserFeatureRepository";
import type { ISelectedCalendarRepository } from "@calcom/features/selectedCalendar/repositories/SelectedCalendarRepository.interface";
import type { SelectedCalendar } from "@calcom/prisma/client";
import { CalendarSubscriptionService } from "../CalendarSubscriptionService";
const mockSelectedCalendar: SelectedCalendar = {
@@ -98,7 +99,11 @@ describe("CalendarSubscriptionService", () => {
let service: CalendarSubscriptionService;
let mockAdapterFactory: AdapterFactory;
let mockSelectedCalendarRepository: ISelectedCalendarRepository;
let mockFeaturesRepository: FeaturesRepository;
let mockFeatureRepository: IFeatureRepository;
let mockTeamFeatureRepository: ITeamFeatureRepository;
let mockUserFeatureRepository: IUserFeatureRepository & {
checkIfUserHasFeature: (userId: number, slug: string) => Promise<boolean>;
};
let mockCalendarCacheEventService: CalendarCacheEventService;
let mockCalendarSyncService: CalendarSyncService;
let mockAdapter: {
@@ -121,12 +126,14 @@ describe("CalendarSubscriptionService", () => {
mockAdapterFactory = {
get: vi.fn().mockReturnValue(mockAdapter),
getProviders: vi.fn().mockReturnValue(["google_calendar", "office365_calendar"]),
getGenericCalendarSuffixes: vi.fn().mockReturnValue([
"@group.v.calendar.google.com",
"@group.calendar.google.com",
"@import.calendar.google.com",
"@resource.calendar.google.com",
]),
getGenericCalendarSuffixes: vi
.fn()
.mockReturnValue([
"@group.v.calendar.google.com",
"@group.calendar.google.com",
"@import.calendar.google.com",
"@resource.calendar.google.com",
]),
};
mockSelectedCalendarRepository = {
@@ -137,13 +144,23 @@ describe("CalendarSubscriptionService", () => {
updateSubscription: vi.fn().mockResolvedValue(mockSelectedCalendar),
};
mockFeaturesRepository = {
mockFeatureRepository = {
findAll: vi.fn(),
findBySlug: vi.fn(),
update: vi.fn(),
checkIfFeatureIsEnabledGlobally: vi.fn().mockResolvedValue(true),
checkIfUserHasFeature: vi.fn().mockResolvedValue(true),
};
mockTeamFeatureRepository = {
checkIfTeamHasFeature: vi.fn().mockResolvedValue(true),
getTeamsWithFeatureEnabled: vi.fn().mockResolvedValue([1, 2, 3]),
};
mockUserFeatureRepository = {
checkIfUserHasFeature: vi.fn().mockResolvedValue(true),
checkIfUserHasFeatureNonHierarchical: vi.fn().mockResolvedValue(true),
};
mockCalendarCacheEventService = {
handleEvents: vi.fn().mockResolvedValue(undefined),
cleanupCache: vi.fn().mockResolvedValue(undefined),
@@ -157,7 +174,9 @@ describe("CalendarSubscriptionService", () => {
service = new CalendarSubscriptionService({
adapterFactory: mockAdapterFactory,
selectedCalendarRepository: mockSelectedCalendarRepository,
featuresRepository: mockFeaturesRepository,
featureRepository: mockFeatureRepository,
teamFeatureRepository: mockTeamFeatureRepository,
userFeatureRepository: mockUserFeatureRepository,
calendarCacheEventService: mockCalendarCacheEventService,
calendarSyncService: mockCalendarSyncService,
});
@@ -237,7 +256,7 @@ describe("CalendarSubscriptionService", () => {
describe("unsubscribe", () => {
test("should successfully unsubscribe from a calendar", async () => {
mockFeaturesRepository.checkIfFeatureIsEnabledGlobally.mockResolvedValue(true);
mockFeatureRepository.checkIfFeatureIsEnabledGlobally.mockResolvedValue(true);
await service.unsubscribe("test-calendar-id");
@@ -250,7 +269,7 @@ describe("CalendarSubscriptionService", () => {
});
test("should not cleanup cache if cache is disabled", async () => {
mockFeaturesRepository.checkIfFeatureIsEnabledGlobally.mockResolvedValue(false);
mockFeatureRepository.checkIfFeatureIsEnabledGlobally.mockResolvedValue(false);
await service.unsubscribe("test-calendar-id");
@@ -307,10 +326,10 @@ describe("CalendarSubscriptionService", () => {
describe("processEvents", () => {
test("should process events when both cache and sync are enabled", async () => {
mockFeaturesRepository.checkIfFeatureIsEnabledGlobally
mockFeatureRepository.checkIfFeatureIsEnabledGlobally
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(true);
mockFeaturesRepository.checkIfUserHasFeature.mockResolvedValue(true);
mockUserFeatureRepository.checkIfUserHasFeature.mockResolvedValue(true);
await service.processEvents(mockSelectedCalendar);
@@ -332,10 +351,10 @@ describe("CalendarSubscriptionService", () => {
});
test("should not process cache when cache is disabled globally", async () => {
mockFeaturesRepository.checkIfFeatureIsEnabledGlobally
mockFeatureRepository.checkIfFeatureIsEnabledGlobally
.mockResolvedValueOnce(false)
.mockResolvedValueOnce(true);
mockFeaturesRepository.checkIfUserHasFeature.mockResolvedValue(true);
mockUserFeatureRepository.checkIfUserHasFeature.mockResolvedValue(true);
await service.processEvents(mockSelectedCalendar);
@@ -344,10 +363,10 @@ describe("CalendarSubscriptionService", () => {
});
test("should not process cache when cache is disabled for user", async () => {
mockFeaturesRepository.checkIfFeatureIsEnabledGlobally
mockFeatureRepository.checkIfFeatureIsEnabledGlobally
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(true);
mockFeaturesRepository.checkIfUserHasFeature.mockResolvedValue(false);
mockUserFeatureRepository.checkIfUserHasFeature.mockResolvedValue(false);
await service.processEvents(mockSelectedCalendar);
@@ -356,7 +375,7 @@ describe("CalendarSubscriptionService", () => {
});
test("should return early when both cache and sync are disabled", async () => {
mockFeaturesRepository.checkIfFeatureIsEnabledGlobally
mockFeatureRepository.checkIfFeatureIsEnabledGlobally
.mockResolvedValueOnce(false)
.mockResolvedValueOnce(false);
@@ -368,10 +387,10 @@ describe("CalendarSubscriptionService", () => {
});
test("should handle API errors and update sync status", async () => {
mockFeaturesRepository.checkIfFeatureIsEnabledGlobally
mockFeatureRepository.checkIfFeatureIsEnabledGlobally
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(true);
mockFeaturesRepository.checkIfUserHasFeature.mockResolvedValue(true);
mockUserFeatureRepository.checkIfUserHasFeature.mockResolvedValue(true);
const apiError = new Error("API Error");
mockAdapter.fetchEvents.mockRejectedValue(apiError);
@@ -387,10 +406,10 @@ describe("CalendarSubscriptionService", () => {
});
test("should return early when no events are fetched", async () => {
mockFeaturesRepository.checkIfFeatureIsEnabledGlobally
mockFeatureRepository.checkIfFeatureIsEnabledGlobally
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(true);
mockFeaturesRepository.checkIfUserHasFeature.mockResolvedValue(true);
mockUserFeatureRepository.checkIfUserHasFeature.mockResolvedValue(true);
mockAdapter.fetchEvents.mockResolvedValue({
...mockEvents,
@@ -422,10 +441,13 @@ describe("CalendarSubscriptionService", () => {
await service.checkForNewSubscriptions();
expect(mockTeamFeatureRepository.getTeamsWithFeatureEnabled).toHaveBeenCalledWith(
"calendar-subscription-cache"
);
expect(mockSelectedCalendarRepository.findNextSubscriptionBatch).toHaveBeenCalledWith({
take: 100,
integrations: ["google_calendar", "office365_calendar"],
featureIds: ["calendar-subscription-cache"],
teamIds: [1, 2, 3],
genericCalendarSuffixes: [
"@group.v.calendar.google.com",
"@group.calendar.google.com",
@@ -449,10 +471,13 @@ describe("CalendarSubscriptionService", () => {
await service.checkForNewSubscriptions();
expect(mockTeamFeatureRepository.getTeamsWithFeatureEnabled).toHaveBeenCalledWith(
"calendar-subscription-cache"
);
expect(mockSelectedCalendarRepository.findNextSubscriptionBatch).toHaveBeenCalledWith({
take: 100,
integrations: ["google_calendar", "office365_calendar"],
featureIds: ["calendar-subscription-cache"],
teamIds: [1, 2, 3],
genericCalendarSuffixes: [
"@group.v.calendar.google.com",
"@group.calendar.google.com",
@@ -465,6 +490,20 @@ describe("CalendarSubscriptionService", () => {
expect(subscribeSpy).toHaveBeenCalledWith("calendar-with-cache-2");
});
test("should skip when cache feature is globally disabled", async () => {
mockFeatureRepository.checkIfFeatureIsEnabledGlobally.mockResolvedValue(false);
const subscribeSpy = vi.spyOn(service, "subscribe").mockResolvedValue(undefined);
await service.checkForNewSubscriptions();
expect(mockFeatureRepository.checkIfFeatureIsEnabledGlobally).toHaveBeenCalledWith(
"calendar-subscription-cache"
);
expect(mockTeamFeatureRepository.getTeamsWithFeatureEnabled).not.toHaveBeenCalled();
expect(mockSelectedCalendarRepository.findNextSubscriptionBatch).not.toHaveBeenCalled();
expect(subscribeSpy).not.toHaveBeenCalled();
});
test("should not process any calendars when no calendars are returned", async () => {
mockSelectedCalendarRepository.findNextSubscriptionBatch.mockResolvedValue([]);
@@ -472,10 +511,13 @@ describe("CalendarSubscriptionService", () => {
await service.checkForNewSubscriptions();
expect(mockTeamFeatureRepository.getTeamsWithFeatureEnabled).toHaveBeenCalledWith(
"calendar-subscription-cache"
);
expect(mockSelectedCalendarRepository.findNextSubscriptionBatch).toHaveBeenCalledWith({
take: 100,
integrations: ["google_calendar", "office365_calendar"],
featureIds: ["calendar-subscription-cache"],
teamIds: [1, 2, 3],
genericCalendarSuffixes: [
"@group.v.calendar.google.com",
"@group.calendar.google.com",
@@ -489,35 +531,35 @@ describe("CalendarSubscriptionService", () => {
describe("feature flag methods", () => {
test("isCacheEnabled should check global cache feature", async () => {
mockFeaturesRepository.checkIfFeatureIsEnabledGlobally.mockResolvedValue(true);
mockFeatureRepository.checkIfFeatureIsEnabledGlobally.mockResolvedValue(true);
const result = await service.isCacheEnabled();
expect(result).toBe(true);
expect(mockFeaturesRepository.checkIfFeatureIsEnabledGlobally).toHaveBeenCalledWith(
expect(mockFeatureRepository.checkIfFeatureIsEnabledGlobally).toHaveBeenCalledWith(
"calendar-subscription-cache"
);
});
test("isCacheEnabledForUser should check user cache feature", async () => {
mockFeaturesRepository.checkIfUserHasFeature.mockResolvedValue(true);
mockUserFeatureRepository.checkIfUserHasFeature.mockResolvedValue(true);
const result = await service.isCacheEnabledForUser(1);
expect(result).toBe(true);
expect(mockFeaturesRepository.checkIfUserHasFeature).toHaveBeenCalledWith(
expect(mockUserFeatureRepository.checkIfUserHasFeature).toHaveBeenCalledWith(
1,
"calendar-subscription-cache"
);
});
test("isSyncEnabled should check global sync feature", async () => {
mockFeaturesRepository.checkIfFeatureIsEnabledGlobally.mockResolvedValue(true);
mockFeatureRepository.checkIfFeatureIsEnabledGlobally.mockResolvedValue(true);
const result = await service.isSyncEnabled();
expect(result).toBe(true);
expect(mockFeaturesRepository.checkIfFeatureIsEnabledGlobally).toHaveBeenCalledWith(
expect(mockFeatureRepository.checkIfFeatureIsEnabledGlobally).toHaveBeenCalledWith(
"calendar-subscription-sync"
);
});
@@ -1,5 +1,4 @@
import { randomBytes } from "node:crypto";
import { getTeamBillingServiceFactory } from "@calcom/ee/billing/di/containers/Billing";
import { SeatChangeTrackingService } from "@calcom/features/ee/billing/service/seatTracking/SeatChangeTrackingService";
import { deleteWorkfowRemindersOfRemovedMember } from "@calcom/features/ee/teams/lib/deleteWorkflowRemindersOfRemovedMember";
@@ -11,9 +10,9 @@ import { createAProfileForAnExistingUser } from "@calcom/features/profile/lib/cr
import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { deleteDomain } from "@calcom/lib/domainManager/organization";
import logger from "@calcom/lib/logger";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { ErrorWithCode } from "@calcom/lib/errors";
import logger from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
import type { Membership } from "@calcom/prisma/client";
import { Prisma } from "@calcom/prisma/client";
@@ -107,7 +106,7 @@ export class TeamService {
if (!isOrgContext) {
return teamInviteLink;
}
const gettingStartedPath = await OnboardingPathService.getGettingStartedPathWhenInvited(prisma);
const gettingStartedPath = await OnboardingPathService.getGettingStartedPathWhenInvited();
const orgInviteLink = `${WEBAPP_URL}/signup?token=${token}&callbackUrl=${gettingStartedPath}`;
return orgInviteLink;
}
@@ -607,4 +606,4 @@ export class TeamService {
}),
]);
}
}
}
@@ -1,9 +1,7 @@
import { z } from "zod";
import { Memoize, Unmemoize } from "@calcom/features/cache";
import type { TeamFeaturesDto } from "@calcom/lib/dto/TeamFeaturesDto";
import { TeamFeaturesDtoSchema } from "@calcom/lib/dto/TeamFeaturesDto";
import { z } from "zod";
import type { FeatureId, TeamFeatures } from "../config";
import type { ITeamFeatureRepository } from "./PrismaTeamFeatureRepository";
import { booleanSchema } from "./schemas";
@@ -91,4 +89,8 @@ export class CachedTeamFeatureRepository implements ITeamFeatureRepository {
async getEnabledFeatures(teamId: number): Promise<TeamFeatures | null> {
return this.prismaTeamFeatureRepository.getEnabledFeatures(teamId);
}
async getTeamsWithFeatureEnabled(featureId: FeatureId): Promise<number[]> {
return this.prismaTeamFeatureRepository.getTeamsWithFeatureEnabled(featureId);
}
}
@@ -6,6 +6,7 @@ export interface IFeatureRepository {
findAll(): Promise<FeatureDto[]>;
findBySlug(slug: string): Promise<FeatureDto | null>;
update(input: { featureId: FeatureId; enabled: boolean; updatedBy?: number }): Promise<FeatureDto>;
checkIfFeatureIsEnabledGlobally(slug: string): Promise<boolean>;
}
export class PrismaFeatureRepository implements IFeatureRepository {
@@ -67,4 +68,12 @@ export class PrismaFeatureRepository implements IFeatureRepository {
},
});
}
async checkIfFeatureIsEnabledGlobally(slug: string): Promise<boolean> {
const feature = await this.prisma.feature.findUnique({
where: { slug },
select: { enabled: true },
});
return Boolean(feature?.enabled);
}
}
@@ -1,9 +1,7 @@
import { captureException } from "@sentry/nextjs";
import type { TeamFeaturesDto } from "@calcom/lib/dto/TeamFeaturesDto";
import type { PrismaClient } from "@calcom/prisma/client";
import { Prisma } from "@calcom/prisma/client";
import { captureException } from "@sentry/nextjs";
import type { FeatureId, TeamFeatures } from "../config";
export interface ITeamFeatureRepository {
@@ -24,6 +22,7 @@ export interface ITeamFeatureRepository {
setAutoOptIn(teamId: number, enabled: boolean): Promise<void>;
checkIfTeamHasFeature(teamId: number, featureId: FeatureId): Promise<boolean>;
getEnabledFeatures(teamId: number): Promise<TeamFeatures | null>;
getTeamsWithFeatureEnabled(featureId: FeatureId): Promise<number[]>;
}
export class PrismaTeamFeatureRepository implements ITeamFeatureRepository {
@@ -250,4 +249,18 @@ export class PrismaTeamFeatureRepository implements ITeamFeatureRepository {
return features;
}
async getTeamsWithFeatureEnabled(featureId: FeatureId): Promise<number[]> {
const result = await this.prisma.teamFeatures.findMany({
where: {
featureId,
enabled: true,
},
select: {
teamId: true,
},
});
return result.map((tf) => tf.teamId);
}
}
@@ -1,24 +1,20 @@
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import type { PrismaClient } from "@calcom/prisma";
import { getFeatureRepository } from "@calcom/features/di/containers/FeatureRepository";
export class OnboardingPathService {
static async getGettingStartedPath(prisma: PrismaClient): Promise<string> {
const featuresRepository = new FeaturesRepository(prisma);
const onboardingV3Enabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("onboarding-v3");
static async getGettingStartedPath(): Promise<string> {
const featureRepository = getFeatureRepository();
const onboardingV3Enabled = await featureRepository.checkIfFeatureIsEnabledGlobally("onboarding-v3");
return onboardingV3Enabled ? "/onboarding/getting-started" : "/getting-started";
}
static async getGettingStartedPathWhenInvited(prisma: PrismaClient): Promise<string> {
const featuresRepository = new FeaturesRepository(prisma);
const onboardingV3Enabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("onboarding-v3");
static async getGettingStartedPathWhenInvited(): Promise<string> {
const featureRepository = getFeatureRepository();
const onboardingV3Enabled = await featureRepository.checkIfFeatureIsEnabledGlobally("onboarding-v3");
return onboardingV3Enabled ? "/onboarding/personal/settings" : "/getting-started";
}
static async getGettingStartedPathWithParams(
prisma: PrismaClient,
queryParams?: Record<string, string>
): Promise<string> {
const basePath = await OnboardingPathService.getGettingStartedPath(prisma);
static async getGettingStartedPathWithParams(queryParams?: Record<string, string>): Promise<string> {
const basePath = await OnboardingPathService.getGettingStartedPath();
if (!queryParams || Object.keys(queryParams).length === 0) {
return basePath;
@@ -20,21 +20,21 @@ export interface ISelectedCalendarRepository {
* Will check if syncSubscribedAt is null or channelExpiration is greater than current date
* Calendars with recent subscription errors (last 24h) are skipped
* Calendars with 3+ subscription errors are skipped entirely
* Joins with TeamFeatures to filter users belonging to teams with any of the specified features enabled
* Joins with Membership to filter users belonging to teams with the specified team IDs
*
* @param take the number of calendars to take
* @param featureIds the feature IDs to filter teams by (users in teams with any of these features enabled)
* @param teamIds the team IDs to filter by (users in these teams will be included)
* @param integrations the list of integrations
* @param genericCalendarSuffixes the list of generic calendar suffixes to exclude
*/
findNextSubscriptionBatch({
take,
featureIds,
teamIds,
integrations,
genericCalendarSuffixes,
}: {
take: number;
featureIds: string[];
teamIds: number[];
integrations: string[];
genericCalendarSuffixes?: string[];
}): Promise<SelectedCalendar[]>;
@@ -1,8 +1,7 @@
import { describe, test, expect, vi, beforeEach } from "vitest";
import { SelectedCalendarRepository } from "@calcom/features/selectedCalendar/repositories/SelectedCalendarRepository";
import type { PrismaClient } from "@calcom/prisma";
import type { Prisma, SelectedCalendar } from "@calcom/prisma/client";
import { beforeEach, describe, expect, test, vi } from "vitest";
const mockPrismaClient = {
selectedCalendar: {
@@ -117,7 +116,7 @@ describe("SelectedCalendarRepository", () => {
const result = await repository.findNextSubscriptionBatch({
take: 10,
featureIds: ["calendar-subscription-cache"],
teamIds: [1, 2, 3],
integrations: ["google_calendar", "office365_calendar"],
});
@@ -128,14 +127,7 @@ describe("SelectedCalendarRepository", () => {
teams: {
some: {
accepted: true,
team: {
features: {
some: {
featureId: { in: ["calendar-subscription-cache"] },
enabled: true,
},
},
},
teamId: { in: [1, 2, 3] },
},
},
},
@@ -148,10 +140,7 @@ describe("SelectedCalendarRepository", () => {
],
},
{
OR: [
{ syncSubscribedErrorAt: null },
{ syncSubscribedErrorAt: { lt: expect.any(Date) } },
],
OR: [{ syncSubscribedErrorAt: null }, { syncSubscribedErrorAt: { lt: expect.any(Date) } }],
},
{
syncSubscribedErrorCount: { lt: 3 },
@@ -170,7 +159,7 @@ describe("SelectedCalendarRepository", () => {
const result = await repository.findNextSubscriptionBatch({
take: 5,
featureIds: ["calendar-subscription-cache"],
teamIds: [1, 2, 3],
integrations: [],
});
@@ -181,14 +170,7 @@ describe("SelectedCalendarRepository", () => {
teams: {
some: {
accepted: true,
team: {
features: {
some: {
featureId: { in: ["calendar-subscription-cache"] },
enabled: true,
},
},
},
teamId: { in: [1, 2, 3] },
},
},
},
@@ -201,10 +183,7 @@ describe("SelectedCalendarRepository", () => {
],
},
{
OR: [
{ syncSubscribedErrorAt: null },
{ syncSubscribedErrorAt: { lt: expect.any(Date) } },
],
OR: [{ syncSubscribedErrorAt: null }, { syncSubscribedErrorAt: { lt: expect.any(Date) } }],
},
{
syncSubscribedErrorCount: { lt: 3 },
@@ -225,7 +204,7 @@ describe("SelectedCalendarRepository", () => {
const result = await repository.findNextSubscriptionBatch({
take: 10,
featureIds: ["calendar-subscription-cache"],
teamIds: [1, 2, 3],
integrations: ["google_calendar"],
genericCalendarSuffixes: genericSuffixes,
});
@@ -237,14 +216,7 @@ describe("SelectedCalendarRepository", () => {
teams: {
some: {
accepted: true,
team: {
features: {
some: {
featureId: { in: ["calendar-subscription-cache"] },
enabled: true,
},
},
},
teamId: { in: [1, 2, 3] },
},
},
},
@@ -257,10 +229,7 @@ describe("SelectedCalendarRepository", () => {
],
},
{
OR: [
{ syncSubscribedErrorAt: null },
{ syncSubscribedErrorAt: { lt: expect.any(Date) } },
],
OR: [{ syncSubscribedErrorAt: null }, { syncSubscribedErrorAt: { lt: expect.any(Date) } }],
},
{
syncSubscribedErrorCount: { lt: 3 },
@@ -19,12 +19,12 @@ export class SelectedCalendarRepository implements ISelectedCalendarRepository {
async findNextSubscriptionBatch({
take,
featureIds,
teamIds,
integrations,
genericCalendarSuffixes,
}: {
take: number;
featureIds: string[];
teamIds: number[];
integrations: string[];
genericCalendarSuffixes?: string[];
}) {
@@ -32,11 +32,7 @@ export class SelectedCalendarRepository implements ISelectedCalendarRepository {
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const needsSubscriptionFilter: Prisma.SelectedCalendarWhereInput = {
OR: [
{ syncSubscribedAt: null },
{ channelExpiration: null },
{ channelExpiration: { lte: now } },
],
OR: [{ syncSubscribedAt: null }, { channelExpiration: null }, { channelExpiration: { lte: now } }],
};
const retryableWindowFilter: Prisma.SelectedCalendarWhereInput = {
@@ -66,14 +62,7 @@ export class SelectedCalendarRepository implements ISelectedCalendarRepository {
teams: {
some: {
accepted: true,
team: {
features: {
some: {
featureId: { in: featureIds },
enabled: true,
},
},
},
teamId: { in: teamIds },
},
},
},
@@ -24,7 +24,6 @@ import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
import type { TFunction } from "i18next";
import { isEmail } from "../util";
import type { TeamWithParent } from "./types";
@@ -541,7 +540,7 @@ export async function sendSignupToOrganizationEmail({
}) {
try {
const verificationToken = await createVerificationToken(usernameOrEmail, teamId);
const gettingStartedPath = await OnboardingPathService.getGettingStartedPathWhenInvited(prisma);
const gettingStartedPath = await OnboardingPathService.getGettingStartedPathWhenInvited();
await sendTeamInviteEmail({
language: translation,
from: inviterName || `${team.name}'s admin`,
@@ -758,7 +757,7 @@ export const sendExistingUserTeamInviteEmails = async ({
if (!user.completedOnboarding && !user.password?.hash && user.identityProvider === "CAL") {
const verificationToken = await createVerificationToken(user.email, teamId);
const gettingStartedPath = await OnboardingPathService.getGettingStartedPathWhenInvited(prisma);
const gettingStartedPath = await OnboardingPathService.getGettingStartedPathWhenInvited();
inviteTeamOptions.joinLink = `${WEBAPP_URL}/signup?token=${verificationToken.token}&callbackUrl=${gettingStartedPath}`;
inviteTeamOptions.isCalcomMember = false;
} else if (!isAutoJoin) {
@@ -1,12 +1,11 @@
import { sendTeamInviteEmail } from "@calcom/emails/organization-email-service";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { OnboardingPathService } from "@calcom/features/onboarding/lib/onboarding-path.service";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getTranslation } from "@calcom/lib/server/i18n";
import { VerificationTokenRepository } from "@calcom/lib/server/repository/verificationToken";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { ensureAtleastAdminPermissions, getTeamOrThrow } from "./inviteMember/utils";
import type { TResendInvitationInputSchema } from "./resendInvitation.schema";
@@ -27,7 +26,9 @@ export const resendInvitationHandler = async ({ ctx, input }: InviteMemberOption
isOrg: input.isOrg,
});
let verificationToken: Awaited<ReturnType<typeof VerificationTokenRepository.updateTeamInviteTokenExpirationDate>> | undefined;
let verificationToken:
| Awaited<ReturnType<typeof VerificationTokenRepository.updateTeamInviteTokenExpirationDate>>
| undefined;
try {
verificationToken = await VerificationTokenRepository.updateTeamInviteTokenExpirationDate({
@@ -52,7 +53,7 @@ export const resendInvitationHandler = async ({ ctx, input }: InviteMemberOption
if (user?.completedOnboarding) {
inviteTeamOptions.joinLink = `${WEBAPP_URL}/teams?token=${verificationToken.token}&autoAccept=true`;
} else {
const gettingStartedPath = await OnboardingPathService.getGettingStartedPathWhenInvited(prisma);
const gettingStartedPath = await OnboardingPathService.getGettingStartedPathWhenInvited();
inviteTeamOptions.joinLink = `${WEBAPP_URL}/signup?token=${verificationToken.token}&callbackUrl=${gettingStartedPath}`;
inviteTeamOptions.isCalcomMember = false;
}