 1a79e0624c
			
		
	
	
		1a79e0624c
		
			
		
	
	
	
	
		
			
			* Init dev * UI changes for recurring event + prisma * Revisiting schema + changes WIP * UI done, BE WIP * Feature completion * Unused query param removed * Invalid comment removed * Removed unused translation * Update apps/web/public/static/locales/en/common.json Thanks! Co-authored-by: Peer Richelsen <peeroke@gmail.com> * Success page changes * More progress * Email text tweaks + test + seed * Tweaking emails + Cal Apps support WIP * No app integration for now Final email and pages tweaks to avoid recurring info showed * Missing comment for clarity * Yet again, comment * Last minute fix * Missing tooltip for upcoming bookings * Fixing seed * Fixing import * Increasing timeout for e2e * Fixing any * Apply suggestions from code review Co-authored-by: Omar López <zomars@me.com> * Update apps/web/pages/d/[link]/book.tsx Co-authored-by: Omar López <zomars@me.com> * Code improvements * More code improvements * Reverting back number input arrows * Update BookingPage.tsx * Update BookingPage.tsx * Adds fallback for sendOrganizerPaymentRefundFailedEmail * Type overkill * Type fixes * Type fixes * Nitpicks * Update success.tsx * Update success.tsx * Update success.tsx * Fixing types Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Omar López <zomars@me.com>
		
			
				
	
	
		
			560 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			560 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { MembershipRole, Prisma, UserPlan } from "@prisma/client";
 | ||
| import dayjs from "dayjs";
 | ||
| import { uuid } from "short-uuid";
 | ||
| 
 | ||
| import { hashPassword } from "@calcom/lib/auth";
 | ||
| import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
 | ||
| 
 | ||
| import prisma from ".";
 | ||
| import "./seed-app-store";
 | ||
| 
 | ||
| require("dotenv").config({ path: "../../.env" });
 | ||
| 
 | ||
| async function createUserAndEventType(opts: {
 | ||
|   user: {
 | ||
|     email: string;
 | ||
|     password: string;
 | ||
|     username: string;
 | ||
|     plan: UserPlan;
 | ||
|     name: string;
 | ||
|     completedOnboarding?: boolean;
 | ||
|     timeZone?: string;
 | ||
|   };
 | ||
|   eventTypes: Array<
 | ||
|     Prisma.EventTypeCreateInput & {
 | ||
|       _bookings?: Prisma.BookingCreateInput[];
 | ||
|     }
 | ||
|   >;
 | ||
| }) {
 | ||
|   const userData = {
 | ||
|     ...opts.user,
 | ||
|     password: await hashPassword(opts.user.password),
 | ||
|     emailVerified: new Date(),
 | ||
|     completedOnboarding: opts.user.completedOnboarding ?? true,
 | ||
|     locale: "en",
 | ||
|     schedules:
 | ||
|       opts.user.completedOnboarding ?? true
 | ||
|         ? {
 | ||
|             create: {
 | ||
|               name: "Working Hours",
 | ||
|               availability: {
 | ||
|                 createMany: {
 | ||
|                   data: getAvailabilityFromSchedule(DEFAULT_SCHEDULE),
 | ||
|                 },
 | ||
|               },
 | ||
|             },
 | ||
|           }
 | ||
|         : undefined,
 | ||
|   };
 | ||
| 
 | ||
|   const user = await prisma.user.upsert({
 | ||
|     where: { email: opts.user.email },
 | ||
|     update: userData,
 | ||
|     create: userData,
 | ||
|   });
 | ||
| 
 | ||
|   console.log(
 | ||
|     `👤 Upserted '${opts.user.username}' with email "${opts.user.email}" & password "${opts.user.password}". Booking page 👉 ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${opts.user.username}`
 | ||
|   );
 | ||
| 
 | ||
|   for (const eventTypeInput of opts.eventTypes) {
 | ||
|     const { _bookings: bookingInputs = [], ...eventTypeData } = eventTypeInput;
 | ||
|     eventTypeData.userId = user.id;
 | ||
|     eventTypeData.users = { connect: { id: user.id } };
 | ||
| 
 | ||
|     const eventType = await prisma.eventType.findFirst({
 | ||
|       where: {
 | ||
|         slug: eventTypeData.slug,
 | ||
|         users: {
 | ||
|           some: {
 | ||
|             id: eventTypeData.userId,
 | ||
|           },
 | ||
|         },
 | ||
|       },
 | ||
|       select: {
 | ||
|         id: true,
 | ||
|       },
 | ||
|     });
 | ||
| 
 | ||
|     if (eventType) {
 | ||
|       console.log(
 | ||
|         `\t📆 Event type ${eventTypeData.slug} already seems seeded - ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user.username}/${eventTypeData.slug}`
 | ||
|       );
 | ||
|       continue;
 | ||
|     }
 | ||
|     const { id } = await prisma.eventType.create({
 | ||
|       data: eventTypeData,
 | ||
|     });
 | ||
| 
 | ||
|     console.log(
 | ||
|       `\t📆 Event type ${eventTypeData.slug}, length ${eventTypeData.length}min - ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user.username}/${eventTypeData.slug}`
 | ||
|     );
 | ||
|     for (const bookingInput of bookingInputs) {
 | ||
|       await prisma.booking.create({
 | ||
|         data: {
 | ||
|           ...bookingInput,
 | ||
|           user: {
 | ||
|             connect: {
 | ||
|               email: opts.user.email,
 | ||
|             },
 | ||
|           },
 | ||
|           attendees: {
 | ||
|             create: {
 | ||
|               email: opts.user.email,
 | ||
|               name: opts.user.name,
 | ||
|               timeZone: "Europe/London",
 | ||
|             },
 | ||
|           },
 | ||
|           eventType: {
 | ||
|             connect: {
 | ||
|               id,
 | ||
|             },
 | ||
|           },
 | ||
|           confirmed: bookingInput.confirmed,
 | ||
|         },
 | ||
|       });
 | ||
|       console.log(
 | ||
|         `\t\t☎️ Created booking ${bookingInput.title} at ${new Date(
 | ||
|           bookingInput.startTime
 | ||
|         ).toLocaleDateString()}`
 | ||
|       );
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   return user;
 | ||
| }
 | ||
| 
 | ||
| async function createTeamAndAddUsers(
 | ||
|   teamInput: Prisma.TeamCreateInput,
 | ||
|   users: { id: number; username: string; role?: MembershipRole }[]
 | ||
| ) {
 | ||
|   const createTeam = async (team: Prisma.TeamCreateInput) => {
 | ||
|     try {
 | ||
|       return await prisma.team.create({
 | ||
|         data: {
 | ||
|           ...team,
 | ||
|         },
 | ||
|       });
 | ||
|     } catch (_err) {
 | ||
|       if (_err instanceof Error && _err.message.indexOf("Unique constraint failed on the fields") !== -1) {
 | ||
|         console.log(`Team '${team.name}' already exists, skipping.`);
 | ||
|         return;
 | ||
|       }
 | ||
|       throw _err;
 | ||
|     }
 | ||
|   };
 | ||
| 
 | ||
|   const team = await createTeam(teamInput);
 | ||
|   if (!team) {
 | ||
|     return;
 | ||
|   }
 | ||
| 
 | ||
|   console.log(
 | ||
|     `🏢 Created team '${teamInput.name}' - ${process.env.NEXT_PUBLIC_WEBAPP_URL}/team/${team.slug}`
 | ||
|   );
 | ||
| 
 | ||
|   for (const user of users) {
 | ||
|     const { role = MembershipRole.OWNER, id, username } = user;
 | ||
|     await prisma.membership.create({
 | ||
|       data: {
 | ||
|         teamId: team.id,
 | ||
|         userId: id,
 | ||
|         role: role,
 | ||
|         accepted: true,
 | ||
|       },
 | ||
|     });
 | ||
|     console.log(`\t👤 Added '${teamInput.name}' membership for '${username}' with role '${role}'`);
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| async function main() {
 | ||
|   await createUserAndEventType({
 | ||
|     user: {
 | ||
|       email: "delete-me@example.com",
 | ||
|       password: "delete-me",
 | ||
|       username: "delete-me",
 | ||
|       name: "delete-me",
 | ||
|       plan: "FREE",
 | ||
|     },
 | ||
|     eventTypes: [],
 | ||
|   });
 | ||
| 
 | ||
|   await createUserAndEventType({
 | ||
|     user: {
 | ||
|       email: "onboarding@example.com",
 | ||
|       password: "onboarding",
 | ||
|       username: "onboarding",
 | ||
|       name: "onboarding",
 | ||
|       plan: "TRIAL",
 | ||
|       completedOnboarding: false,
 | ||
|     },
 | ||
|     eventTypes: [],
 | ||
|   });
 | ||
| 
 | ||
|   await createUserAndEventType({
 | ||
|     user: {
 | ||
|       email: "free-first-hidden@example.com",
 | ||
|       password: "free-first-hidden",
 | ||
|       username: "free-first-hidden",
 | ||
|       name: "Free First Hidden Example",
 | ||
|       plan: "FREE",
 | ||
|     },
 | ||
|     eventTypes: [
 | ||
|       {
 | ||
|         title: "30min",
 | ||
|         slug: "30min",
 | ||
|         length: 30,
 | ||
|         hidden: true,
 | ||
|       },
 | ||
|       {
 | ||
|         title: "60min",
 | ||
|         slug: "60min",
 | ||
|         length: 30,
 | ||
|       },
 | ||
|     ],
 | ||
|   });
 | ||
|   await createUserAndEventType({
 | ||
|     user: {
 | ||
|       email: "pro@example.com",
 | ||
|       name: "Pro Example",
 | ||
|       password: "pro",
 | ||
|       username: "pro",
 | ||
|       plan: "PRO",
 | ||
|     },
 | ||
|     eventTypes: [
 | ||
|       {
 | ||
|         title: "30min",
 | ||
|         slug: "30min",
 | ||
|         length: 30,
 | ||
|         _bookings: [
 | ||
|           {
 | ||
|             uid: uuid(),
 | ||
|             title: "30min",
 | ||
|             startTime: dayjs().add(1, "day").toDate(),
 | ||
|             endTime: dayjs().add(1, "day").add(30, "minutes").toDate(),
 | ||
|           },
 | ||
|           {
 | ||
|             uid: uuid(),
 | ||
|             title: "30min",
 | ||
|             startTime: dayjs().add(2, "day").toDate(),
 | ||
|             endTime: dayjs().add(2, "day").add(30, "minutes").toDate(),
 | ||
|             confirmed: false,
 | ||
|           },
 | ||
|         ],
 | ||
|       },
 | ||
|       {
 | ||
|         title: "60min",
 | ||
|         slug: "60min",
 | ||
|         length: 60,
 | ||
|       },
 | ||
|       {
 | ||
|         title: "paid",
 | ||
|         slug: "paid",
 | ||
|         length: 60,
 | ||
|         price: 100,
 | ||
|       },
 | ||
|       {
 | ||
|         title: "In person meeting",
 | ||
|         slug: "in-person",
 | ||
|         length: 60,
 | ||
|         locations: [{ type: "inPerson", address: "London" }],
 | ||
|       },
 | ||
|       {
 | ||
|         title: "Zoom Event",
 | ||
|         slug: "zoom",
 | ||
|         length: 60,
 | ||
|         locations: [{ type: "integrations:zoom" }],
 | ||
|       },
 | ||
|       {
 | ||
|         title: "Daily Event",
 | ||
|         slug: "daily",
 | ||
|         length: 60,
 | ||
|         locations: [{ type: "integrations:daily" }],
 | ||
|       },
 | ||
|       {
 | ||
|         title: "Google Meet",
 | ||
|         slug: "google-meet",
 | ||
|         length: 60,
 | ||
|         locations: [{ type: "integrations:google:meet" }],
 | ||
|       },
 | ||
|       {
 | ||
|         title: "Yoga class",
 | ||
|         slug: "yoga-class",
 | ||
|         length: 30,
 | ||
|         recurringEvent: { freq: 2, count: 12, interval: 1 },
 | ||
|         _bookings: [
 | ||
|           {
 | ||
|             uid: uuid(),
 | ||
|             title: "Yoga class",
 | ||
|             recurringEventId: Buffer.from("yoga-class").toString("base64"),
 | ||
|             startTime: dayjs().add(1, "day").toDate(),
 | ||
|             endTime: dayjs().add(1, "day").add(30, "minutes").toDate(),
 | ||
|             confirmed: false,
 | ||
|           },
 | ||
|           {
 | ||
|             uid: uuid(),
 | ||
|             title: "Yoga class",
 | ||
|             recurringEventId: Buffer.from("yoga-class").toString("base64"),
 | ||
|             startTime: dayjs().add(1, "day").add(1, "week").toDate(),
 | ||
|             endTime: dayjs().add(1, "day").add(1, "week").add(30, "minutes").toDate(),
 | ||
|             confirmed: false,
 | ||
|           },
 | ||
|           {
 | ||
|             uid: uuid(),
 | ||
|             title: "Yoga class",
 | ||
|             recurringEventId: Buffer.from("yoga-class").toString("base64"),
 | ||
|             startTime: dayjs().add(1, "day").add(2, "week").toDate(),
 | ||
|             endTime: dayjs().add(1, "day").add(2, "week").add(30, "minutes").toDate(),
 | ||
|             confirmed: false,
 | ||
|           },
 | ||
|           {
 | ||
|             uid: uuid(),
 | ||
|             title: "Yoga class",
 | ||
|             recurringEventId: Buffer.from("yoga-class").toString("base64"),
 | ||
|             startTime: dayjs().add(1, "day").add(3, "week").toDate(),
 | ||
|             endTime: dayjs().add(1, "day").add(3, "week").add(30, "minutes").toDate(),
 | ||
|             confirmed: false,
 | ||
|           },
 | ||
|           {
 | ||
|             uid: uuid(),
 | ||
|             title: "Yoga class",
 | ||
|             recurringEventId: Buffer.from("yoga-class").toString("base64"),
 | ||
|             startTime: dayjs().add(1, "day").add(4, "week").toDate(),
 | ||
|             endTime: dayjs().add(1, "day").add(4, "week").add(30, "minutes").toDate(),
 | ||
|             confirmed: false,
 | ||
|           },
 | ||
|           {
 | ||
|             uid: uuid(),
 | ||
|             title: "Yoga class",
 | ||
|             recurringEventId: Buffer.from("yoga-class").toString("base64"),
 | ||
|             startTime: dayjs().add(1, "day").add(5, "week").toDate(),
 | ||
|             endTime: dayjs().add(1, "day").add(5, "week").add(30, "minutes").toDate(),
 | ||
|             confirmed: false,
 | ||
|           },
 | ||
|         ],
 | ||
|       },
 | ||
|       {
 | ||
|         title: "Tennis class",
 | ||
|         slug: "tennis-class",
 | ||
|         length: 60,
 | ||
|         recurringEvent: { freq: 2, count: 10, interval: 2 },
 | ||
|         requiresConfirmation: true,
 | ||
|         _bookings: [
 | ||
|           {
 | ||
|             uid: uuid(),
 | ||
|             title: "Tennis class",
 | ||
|             recurringEventId: Buffer.from("tennis-class").toString("base64"),
 | ||
|             startTime: dayjs().add(2, "day").toDate(),
 | ||
|             endTime: dayjs().add(2, "day").add(60, "minutes").toDate(),
 | ||
|             confirmed: false,
 | ||
|           },
 | ||
|           {
 | ||
|             uid: uuid(),
 | ||
|             title: "Tennis class",
 | ||
|             recurringEventId: Buffer.from("tennis-class").toString("base64"),
 | ||
|             startTime: dayjs().add(2, "day").add(2, "week").toDate(),
 | ||
|             endTime: dayjs().add(2, "day").add(2, "week").add(60, "minutes").toDate(),
 | ||
|             confirmed: false,
 | ||
|           },
 | ||
|           {
 | ||
|             uid: uuid(),
 | ||
|             title: "Tennis class",
 | ||
|             recurringEventId: Buffer.from("tennis-class").toString("base64"),
 | ||
|             startTime: dayjs().add(2, "day").add(4, "week").toDate(),
 | ||
|             endTime: dayjs().add(2, "day").add(4, "week").add(60, "minutes").toDate(),
 | ||
|             confirmed: false,
 | ||
|           },
 | ||
|           {
 | ||
|             uid: uuid(),
 | ||
|             title: "Tennis class",
 | ||
|             recurringEventId: Buffer.from("tennis-class").toString("base64"),
 | ||
|             startTime: dayjs().add(2, "day").add(8, "week").toDate(),
 | ||
|             endTime: dayjs().add(2, "day").add(8, "week").add(60, "minutes").toDate(),
 | ||
|             confirmed: false,
 | ||
|           },
 | ||
|           {
 | ||
|             uid: uuid(),
 | ||
|             title: "Tennis class",
 | ||
|             recurringEventId: Buffer.from("tennis-class").toString("base64"),
 | ||
|             startTime: dayjs().add(2, "day").add(10, "week").toDate(),
 | ||
|             endTime: dayjs().add(2, "day").add(10, "week").add(60, "minutes").toDate(),
 | ||
|             confirmed: false,
 | ||
|           },
 | ||
|         ],
 | ||
|       },
 | ||
|     ],
 | ||
|   });
 | ||
| 
 | ||
|   await createUserAndEventType({
 | ||
|     user: {
 | ||
|       email: "trial@example.com",
 | ||
|       password: "trial",
 | ||
|       username: "trial",
 | ||
|       name: "Trial Example",
 | ||
|       plan: "TRIAL",
 | ||
|     },
 | ||
|     eventTypes: [
 | ||
|       {
 | ||
|         title: "30min",
 | ||
|         slug: "30min",
 | ||
|         length: 30,
 | ||
|       },
 | ||
|       {
 | ||
|         title: "60min",
 | ||
|         slug: "60min",
 | ||
|         length: 60,
 | ||
|       },
 | ||
|     ],
 | ||
|   });
 | ||
| 
 | ||
|   await createUserAndEventType({
 | ||
|     user: {
 | ||
|       email: "free@example.com",
 | ||
|       password: "free",
 | ||
|       username: "free",
 | ||
|       name: "Free Example",
 | ||
|       plan: "FREE",
 | ||
|     },
 | ||
|     eventTypes: [
 | ||
|       {
 | ||
|         title: "30min",
 | ||
|         slug: "30min",
 | ||
|         length: 30,
 | ||
|       },
 | ||
|       {
 | ||
|         title: "60min",
 | ||
|         slug: "60min",
 | ||
|         length: 30,
 | ||
|       },
 | ||
|     ],
 | ||
|   });
 | ||
| 
 | ||
|   await createUserAndEventType({
 | ||
|     user: {
 | ||
|       email: "usa@example.com",
 | ||
|       password: "usa",
 | ||
|       username: "usa",
 | ||
|       name: "USA Timezone Example",
 | ||
|       plan: "FREE",
 | ||
|       timeZone: "America/Phoenix",
 | ||
|     },
 | ||
|     eventTypes: [
 | ||
|       {
 | ||
|         title: "30min",
 | ||
|         slug: "30min",
 | ||
|         length: 30,
 | ||
|       },
 | ||
|     ],
 | ||
|   });
 | ||
| 
 | ||
|   const freeUserTeam = await createUserAndEventType({
 | ||
|     user: {
 | ||
|       email: "teamfree@example.com",
 | ||
|       password: "teamfree",
 | ||
|       username: "teamfree",
 | ||
|       name: "Team Free Example",
 | ||
|       plan: "FREE",
 | ||
|     },
 | ||
|     eventTypes: [],
 | ||
|   });
 | ||
| 
 | ||
|   const proUserTeam = await createUserAndEventType({
 | ||
|     user: {
 | ||
|       email: "teampro@example.com",
 | ||
|       password: "teampro",
 | ||
|       username: "teampro",
 | ||
|       name: "Team Pro Example",
 | ||
|       plan: "PRO",
 | ||
|     },
 | ||
|     eventTypes: [],
 | ||
|   });
 | ||
| 
 | ||
|   const pro2UserTeam = await createUserAndEventType({
 | ||
|     user: {
 | ||
|       email: "teampro2@example.com",
 | ||
|       password: "teampro2",
 | ||
|       username: "teampro2",
 | ||
|       name: "Team Pro Example 2",
 | ||
|       plan: "PRO",
 | ||
|     },
 | ||
|     eventTypes: [],
 | ||
|   });
 | ||
| 
 | ||
|   const pro3UserTeam = await createUserAndEventType({
 | ||
|     user: {
 | ||
|       email: "teampro3@example.com",
 | ||
|       password: "teampro3",
 | ||
|       username: "teampro3",
 | ||
|       name: "Team Pro Example 3",
 | ||
|       plan: "PRO",
 | ||
|     },
 | ||
|     eventTypes: [],
 | ||
|   });
 | ||
| 
 | ||
|   const pro4UserTeam = await createUserAndEventType({
 | ||
|     user: {
 | ||
|       email: "teampro4@example.com",
 | ||
|       password: "teampro4",
 | ||
|       username: "teampro4",
 | ||
|       name: "Team Pro Example 4",
 | ||
|       plan: "PRO",
 | ||
|     },
 | ||
|     eventTypes: [],
 | ||
|   });
 | ||
| 
 | ||
|   await createTeamAndAddUsers(
 | ||
|     {
 | ||
|       name: "Seeded Team",
 | ||
|       slug: "seeded-team",
 | ||
|       eventTypes: {
 | ||
|         createMany: {
 | ||
|           data: [
 | ||
|             {
 | ||
|               title: "Collective Seeded Team Event",
 | ||
|               slug: "collective-seeded-team-event",
 | ||
|               length: 15,
 | ||
|               schedulingType: "COLLECTIVE",
 | ||
|             },
 | ||
|             {
 | ||
|               title: "Round Robin Seeded Team Event",
 | ||
|               slug: "round-robin-seeded-team-event",
 | ||
|               length: 15,
 | ||
|               schedulingType: "ROUND_ROBIN",
 | ||
|             },
 | ||
|           ],
 | ||
|         },
 | ||
|       },
 | ||
|     },
 | ||
|     [
 | ||
|       {
 | ||
|         id: proUserTeam.id,
 | ||
|         username: proUserTeam.name || "Unknown",
 | ||
|       },
 | ||
|       {
 | ||
|         id: freeUserTeam.id,
 | ||
|         username: freeUserTeam.name || "Unknown",
 | ||
|       },
 | ||
|       {
 | ||
|         id: pro2UserTeam.id,
 | ||
|         username: pro2UserTeam.name || "Unknown",
 | ||
|       },
 | ||
|       {
 | ||
|         id: pro3UserTeam.id,
 | ||
|         username: pro3UserTeam.name || "Unknown",
 | ||
|       },
 | ||
|       {
 | ||
|         id: pro4UserTeam.id,
 | ||
|         username: pro4UserTeam.name || "Unknown",
 | ||
|       },
 | ||
|     ]
 | ||
|   );
 | ||
| }
 | ||
| 
 | ||
| main()
 | ||
|   .catch((e) => {
 | ||
|     console.error(e);
 | ||
|     process.exit(1);
 | ||
|   })
 | ||
|   .finally(async () => {
 | ||
|     await prisma.$disconnect();
 | ||
|   });
 |