diff --git a/apps/web/@prisma/client.ts b/apps/web/@prisma/client.ts
deleted file mode 100644
index 8391ec8e..00000000
--- a/apps/web/@prisma/client.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "@calcom/prisma/client";
diff --git a/apps/web/ee/components/TrialBanner.tsx b/apps/web/ee/components/TrialBanner.tsx
index bb4575af..ed83d826 100644
--- a/apps/web/ee/components/TrialBanner.tsx
+++ b/apps/web/ee/components/TrialBanner.tsx
@@ -13,9 +13,11 @@ const TrialBanner = () => {
if (!user || user.plan !== "TRIAL") return null;
- const trialDaysLeft = dayjs(user.createdDate)
- .add(TRIAL_LIMIT_DAYS + 1, "day")
- .diff(dayjs(), "day");
+ const trialDaysLeft = user.trialEndsAt
+ ? dayjs(user.trialEndsAt).add(1, "day").diff(dayjs(), "day")
+ : dayjs(user.createdDate)
+ .add(TRIAL_LIMIT_DAYS + 1, "day")
+ .diff(dayjs(), "day");
return (
&1 | tee result.log`
+import { MembershipRole, Prisma, UserPlan } from "@prisma/client";
+import dayjs from "dayjs";
+
+import prisma from "@calcom/prisma";
+
+import { TRIAL_LIMIT_DAYS } from "@lib/config/constants";
+
+import { getStripeCustomerIdFromUserId } from "./customer";
+import stripe from "./server";
+import { getPremiumPlanPrice, getProPlanPrice } from "./team-billing";
+
+export async function downgradeIllegalProUsers() {
+ const usersDowngraded: string[] = [];
+ const illegalProUsers = await prisma.membership.findMany({
+ where: {
+ role: {
+ not: MembershipRole.OWNER,
+ },
+ user: {
+ plan: {
+ not: UserPlan.PRO,
+ },
+ },
+ },
+ include: {
+ user: true,
+ },
+ });
+ const downgrade = async (member: typeof illegalProUsers[number]) => {
+ console.log(`Downgrading: ${member.user.email}`);
+ await prisma.user.update({
+ where: { id: member.user.id },
+ data: {
+ plan: UserPlan.TRIAL,
+ trialEndsAt: dayjs().add(TRIAL_LIMIT_DAYS, "day").toDate(),
+ },
+ });
+ console.log(`Downgraded: ${member.user.email}`);
+ usersDowngraded.push(member.user.username || `${member.user.id}`);
+ };
+ for (const member of illegalProUsers) {
+ const metadata = (member.user.metadata as Prisma.JsonObject) ?? {};
+ // if their pro is already sponsored by a team, do not downgrade
+ if (metadata.proPaidForTeamId !== undefined) continue;
+
+ const stripeCustomerId = await getStripeCustomerIdFromUserId(member.user.id);
+ if (!stripeCustomerId) {
+ await downgrade(member);
+ continue;
+ }
+
+ const customer = await stripe.customers.retrieve(stripeCustomerId, {
+ expand: ["subscriptions.data.plan"],
+ });
+ if (!customer || customer.deleted) {
+ await downgrade(member);
+ continue;
+ }
+
+ const subscription = customer.subscriptions?.data[0];
+ if (!subscription) {
+ await downgrade(member);
+ continue;
+ }
+
+ const hasProPlan = !!subscription.items.data.find(
+ (item) => item.plan.id === getProPlanPrice() || item.plan.id === getPremiumPlanPrice()
+ );
+ // if they're pro, do not downgrade
+ if (hasProPlan) continue;
+
+ await downgrade(member);
+ }
+ return {
+ usersDowngraded,
+ usersDowngradedAmount: usersDowngraded.length,
+ };
+}
+
+downgradeIllegalProUsers()
+ .then(({ usersDowngraded, usersDowngradedAmount }) => {
+ console.log(`Downgraded ${usersDowngradedAmount} illegal pro users`);
+ console.table(usersDowngraded);
+ })
+ .catch((e) => {
+ console.error(e);
+ process.exit(1);
+ });
diff --git a/apps/web/ee/lib/stripe/server.ts b/apps/web/ee/lib/stripe/server.ts
index eb762be1..41ba27b8 100644
--- a/apps/web/ee/lib/stripe/server.ts
+++ b/apps/web/ee/lib/stripe/server.ts
@@ -2,10 +2,11 @@ import { PaymentType, Prisma } from "@prisma/client";
import Stripe from "stripe";
import { v4 as uuidv4 } from "uuid";
+import prisma from "@calcom/prisma";
+
import { sendAwaitingPaymentEmail, sendOrganizerPaymentRefundFailedEmail } from "@lib/emails/email-manager";
import { getErrorFromUnknown } from "@lib/errors";
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
-import prisma from "@lib/prisma";
import { createPaymentLink } from "./client";
diff --git a/apps/web/ee/lib/stripe/team-billing.ts b/apps/web/ee/lib/stripe/team-billing.ts
index c36f7298..caeee245 100644
--- a/apps/web/ee/lib/stripe/team-billing.ts
+++ b/apps/web/ee/lib/stripe/team-billing.ts
@@ -1,17 +1,17 @@
import { MembershipRole, Prisma, UserPlan } from "@prisma/client";
import Stripe from "stripe";
-import { getStripeCustomerFromUser } from "@ee/lib/stripe/customer";
+import prisma from "@calcom/prisma";
+import { getStripeCustomerIdFromUserId } from "@ee/lib/stripe/customer";
import { HOSTED_CAL_FEATURES } from "@lib/config/constants";
import { HttpError } from "@lib/core/http/error";
-import prisma from "@lib/prisma";
import stripe from "./server";
// get team owner's Pro Plan subscription from Cal userId
export async function getProPlanSubscription(userId: number) {
- const stripeCustomerId = await getStripeCustomerFromUser(userId);
+ const stripeCustomerId = await getStripeCustomerIdFromUserId(userId);
if (!stripeCustomerId) return null;
const customer = await stripe.customers.retrieve(stripeCustomerId, {
@@ -82,11 +82,17 @@ export async function upgradeTeam(userId: number, teamId: number) {
const { membersMissingSeats, ownerIsMissingSeat } = await getMembersMissingSeats(teamId);
if (!subscription) {
- const customer = await getStripeCustomerFromUser(userId);
- if (!customer) throw new HttpError({ statusCode: 400, message: "User has no Stripe customer" });
+ let customerId = await getStripeCustomerIdFromUserId(userId);
+ if (!customerId) {
+ // create stripe customer if it doesn't already exist
+ const res = await stripe.customers.create({
+ email: ownerUser.user.email,
+ });
+ customerId = res.id;
+ }
// create a checkout session with the quantity of missing seats
const session = await createCheckoutSession(
- customer,
+ customerId,
membersMissingSeats.length,
teamId,
ownerIsMissingSeat
@@ -257,19 +263,20 @@ export async function ensureSubscriptionQuantityCorrectness(userId: number, team
}
}
+const isProductionSite =
+ process.env.NEXT_PUBLIC_BASE_URL === "https://app.cal.com" && process.env.VERCEL_ENV === "production";
+
// TODO: these should be moved to env vars
export function getPerSeatProPlanPrice(): string {
- return process.env.NODE_ENV === "production"
- ? "price_1KHkoeH8UDiwIftkkUbiggsM"
- : "price_1KLD4GH8UDiwIftkWQfsh1Vh";
+ return isProductionSite ? "price_1KHkoeH8UDiwIftkkUbiggsM" : "price_1KLD4GH8UDiwIftkWQfsh1Vh";
}
export function getProPlanPrice(): string {
- return process.env.NODE_ENV === "production"
- ? "price_1KHkoeH8UDiwIftkkUbiggsM"
- : "price_1JZ0J3H8UDiwIftk0YIHYKr8";
+ return isProductionSite ? "price_1KHkoeH8UDiwIftkkUbiggsM" : "price_1JZ0J3H8UDiwIftk0YIHYKr8";
}
export function getPremiumPlanPrice(): string {
- return process.env.NODE_ENV === "production"
- ? "price_1Jv3CMH8UDiwIftkFgyXbcHN"
- : "price_1Jv3CMH8UDiwIftkFgyXbcHN";
+ return isProductionSite ? "price_1Jv3CMH8UDiwIftkFgyXbcHN" : "price_1Jv3CMH8UDiwIftkFgyXbcHN";
+}
+
+export function getProPlanProduct(): string {
+ return isProductionSite ? "prod_JVxwoOF5odFiZ8" : "prod_KDRBg0E4HyVZee";
}
diff --git a/apps/web/ee/pages/api/integrations/stripepayment/portal.ts b/apps/web/ee/pages/api/integrations/stripepayment/portal.ts
index 3234bb2a..d1ba8682 100644
--- a/apps/web/ee/pages/api/integrations/stripepayment/portal.ts
+++ b/apps/web/ee/pages/api/integrations/stripepayment/portal.ts
@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from "next";
-import { getStripeCustomerFromUser } from "@ee/lib/stripe/customer";
+import { getStripeCustomerIdFromUserId } from "@ee/lib/stripe/customer";
import stripe from "@ee/lib/stripe/server";
import { getSession } from "@lib/auth";
@@ -15,7 +15,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return;
}
- const customerId = await getStripeCustomerFromUser(session.user.id);
+ const customerId = await getStripeCustomerIdFromUserId(session.user.id);
if (!customerId) {
res.status(500).json({ message: "Missing customer id" });
diff --git a/apps/web/lib/prisma.ts b/apps/web/lib/prisma.ts
index ea146d1a..3f0ea376 100644
--- a/apps/web/lib/prisma.ts
+++ b/apps/web/lib/prisma.ts
@@ -1,20 +1 @@
-import { PrismaClient } from "@prisma/client";
-
-import { IS_PRODUCTION } from "@lib/config/constants";
-
-declare global {
- // eslint-disable-next-line no-var
- var prisma: PrismaClient | undefined;
-}
-
-export const prisma =
- globalThis.prisma ||
- new PrismaClient({
- // log: ["query", "error", "warn"],
- });
-
-if (!IS_PRODUCTION) {
- globalThis.prisma = prisma;
-}
-
-export default prisma;
+export { default } from "@calcom/prisma";
diff --git a/apps/web/package.json b/apps/web/package.json
index 3e6a1148..5f224785 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -18,7 +18,8 @@
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint . --ext .ts,.js,.tsx,.jsx --fix",
- "check-changed-files": "ts-node scripts/ts-check-changed-files.ts"
+ "check-changed-files": "ts-node scripts/ts-check-changed-files.ts",
+ "downgrade": "ts-node ee/lib/stripe/downgrade.ts"
},
"engines": {
"node": ">=14.x",
@@ -135,7 +136,7 @@
"eslint": "^8.9.0",
"tailwindcss": "^3.0.0",
"ts-jest": "^26.0.0",
- "ts-node": "^10.2.1",
+ "ts-node": "^10.6.0",
"typescript": "^4.5.3"
}
}
diff --git a/apps/web/pages/api/cron/downgradeUsers.ts b/apps/web/pages/api/cron/downgradeUsers.ts
index fd60c8d2..1a0fd303 100644
--- a/apps/web/pages/api/cron/downgradeUsers.ts
+++ b/apps/web/pages/api/cron/downgradeUsers.ts
@@ -27,9 +27,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
where: {
plan: "TRIAL",
- createdDate: {
- lt: dayjs().subtract(TRIAL_LIMIT_DAYS, "day").toDate(),
- },
+ OR: [
+ /**
+ * If the user doesn't have a trial end date,
+ * use the default 14 day trial from creation.
+ */
+ {
+ createdDate: {
+ lt: dayjs().subtract(TRIAL_LIMIT_DAYS, "day").toDate(),
+ },
+ trialEndsAt: null,
+ },
+ /** If it does, then honor the trial end date. */
+ {
+ trialEndsAt: {
+ lt: dayjs().toDate(),
+ },
+ },
+ ],
},
});
res.json({ ok: true });
diff --git a/apps/web/server/createContext.ts b/apps/web/server/createContext.ts
index 60544411..b3cabdfb 100644
--- a/apps/web/server/createContext.ts
+++ b/apps/web/server/createContext.ts
@@ -67,6 +67,7 @@ async function getUserFromSession({
destinationCalendar: true,
locale: true,
timeFormat: true,
+ trialEndsAt: true,
},
});
diff --git a/apps/web/server/routers/viewer.tsx b/apps/web/server/routers/viewer.tsx
index de0be464..76cc6c85 100644
--- a/apps/web/server/routers/viewer.tsx
+++ b/apps/web/server/routers/viewer.tsx
@@ -78,6 +78,7 @@ const loggedInViewerRouter = createProtectedRouter()
timeFormat: user.timeFormat,
avatar: user.avatar,
createdDate: user.createdDate,
+ trialEndsAt: user.trialEndsAt,
completedOnboarding: user.completedOnboarding,
twoFactorEnabled: user.twoFactorEnabled,
identityProvider: user.identityProvider,
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
index a8423c25..53588175 100644
--- a/apps/web/tsconfig.json
+++ b/apps/web/tsconfig.json
@@ -6,7 +6,8 @@
"@components/*": ["components/*"],
"@lib/*": ["lib/*"],
"@server/*": ["server/*"],
- "@ee/*": ["ee/*"]
+ "@ee/*": ["ee/*"],
+ "@prisma/client/*": ["@calcom/prisma/client/*"]
},
"typeRoots": ["./types"],
"types": ["@types/jest"]
diff --git a/packages/prisma/index.ts b/packages/prisma/index.ts
index 4b242287..70180d60 100644
--- a/packages/prisma/index.ts
+++ b/packages/prisma/index.ts
@@ -1,7 +1,7 @@
import { PrismaClient } from "@prisma/client";
declare global {
- var prisma: PrismaClient;
+ var prisma: PrismaClient | undefined;
}
export const prisma =
diff --git a/packages/prisma/migrations/20220303171305_adds_user_trial_ends_at/migration.sql b/packages/prisma/migrations/20220303171305_adds_user_trial_ends_at/migration.sql
new file mode 100644
index 00000000..7e411999
--- /dev/null
+++ b/packages/prisma/migrations/20220303171305_adds_user_trial_ends_at/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "users" ADD COLUMN "trialEndsAt" TIMESTAMP(3);
diff --git a/packages/prisma/package.json b/packages/prisma/package.json
index d63d0814..8abdc277 100644
--- a/packages/prisma/package.json
+++ b/packages/prisma/package.json
@@ -20,7 +20,7 @@
"devDependencies": {
"npm-run-all": "^4.1.5",
"prisma": "3.9.2",
- "ts-node": "^10.2.1",
+ "ts-node": "^10.6.0",
"zod-prisma": "^0.5.4"
},
"dependencies": {
@@ -29,6 +29,11 @@
},
"main": "index.ts",
"types": "index.d.ts",
+ "files": [
+ "client",
+ "zod",
+ "zod-utils.ts"
+ ],
"prisma": {
"seed": "ts-node ./seed.ts"
}
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index 9f38dd52..2fefa2b7 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -121,6 +121,7 @@ model User {
hideBranding Boolean @default(false)
theme String?
createdDate DateTime @default(now()) @map(name: "created")
+ trialEndsAt DateTime?
eventTypes EventType[] @relation("user_eventtype")
credentials Credential[]
teams Membership[]
@@ -353,6 +354,6 @@ model Webhook {
createdAt DateTime @default(now())
active Boolean @default(true)
eventTriggers WebhookTriggerEvents[]
- user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
- eventType EventType? @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
+ user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
+ eventType EventType? @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
}
diff --git a/packages/tsconfig/base.json b/packages/tsconfig/base.json
index 0fcd67ee..26743a80 100644
--- a/packages/tsconfig/base.json
+++ b/packages/tsconfig/base.json
@@ -18,6 +18,9 @@
},
"exclude": ["node_modules"],
"ts-node": {
+ "files": true,
+ "require": ["tsconfig-paths/register"],
+ "experimentalResolverFeatures": true,
"compilerOptions": {
"module": "CommonJS",
"types": ["node"]
diff --git a/packages/tsconfig/package.json b/packages/tsconfig/package.json
index 61926e7f..0b6f2ee0 100644
--- a/packages/tsconfig/package.json
+++ b/packages/tsconfig/package.json
@@ -7,5 +7,8 @@
"base.json",
"nextjs.json",
"react-library.json"
- ]
+ ],
+ "devDependencies": {
+ "tsconfig-paths": "^3.12.0"
+ }
}
diff --git a/yarn.lock b/yarn.lock
index 0a8fe979..ab67685c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13900,10 +13900,10 @@ ts-morph@^13.0.2:
"@ts-morph/common" "~0.12.3"
code-block-writer "^11.0.0"
-ts-node@^10.2.1:
- version "10.5.0"
- resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.5.0.tgz#618bef5854c1fbbedf5e31465cbb224a1d524ef9"
- integrity sha512-6kEJKwVxAJ35W4akuiysfKwKmjkbYxwQMTBaAxo9KKAx/Yd26mPUyhGz3ji+EsJoAgrLqVsYHNuuYwQe22lbtw==
+ts-node@^10.6.0:
+ version "10.6.0"
+ resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.6.0.tgz#c3f4195d5173ce3affdc8f2fd2e9a7ac8de5376a"
+ integrity sha512-CJen6+dfOXolxudBQXnVjRVvYTmTWbyz7cn+xq2XTsvnaXbHqr4gXSCNbS2Jj8yTZMuGwUoBESLaOkLascVVvg==
dependencies:
"@cspotcode/source-map-support" "0.7.0"
"@tsconfig/node10" "^1.0.7"