Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 50 additions & 3 deletions packages/app-store/_utils/payments/handlePaymentSuccess.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { eventTypeAppMetadataOptionalSchema } from "@calcom/app-store/zod-utils";
import { sendScheduledEmailsAndSMS } from "@calcom/emails/email-manager";
import { sendScheduledEmailsAndSMS, sendScheduledSeatsEmailsAndSMS } from "@calcom/emails/email-manager";
import EventManager, { placeholderCreatedEvent } from "@calcom/features/bookings/lib/EventManager";
import { doesBookingRequireConfirmation } from "@calcom/features/bookings/lib/doesBookingRequireConfirmation";
import { getAllCredentialsIncludeServiceAccountKey } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials";
import { handleBookingRequested } from "@calcom/features/bookings/lib/handleBookingRequested";
import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation";
import { getBooking } from "@calcom/features/bookings/lib/payment/getBooking";
import {
allowDisablingAttendeeConfirmationEmails,
allowDisablingHostConfirmationEmails,
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
import { getPlatformParams } from "@calcom/features/platform-oauth-client/get-platform-params";
import { PlatformOAuthClientRepository } from "@calcom/features/platform-oauth-client/platform-oauth-client.repository";
import { HttpError as HttpCode } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
import { EventTypeMetaDataSchema, type EventTypeMetadata } from "@calcom/prisma/zod-utils";
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";

const log = logger.getSubLogger({ prefix: ["[handlePaymentSuccess]"] });
export async function handlePaymentSuccess(paymentId: number, bookingId: number) {
Expand Down Expand Up @@ -96,7 +101,49 @@ export async function handlePaymentSuccess(paymentId: number, bookingId: number)
log.debug(`handling booking request for eventId ${eventType.id}`);
}
} else if (areEmailsEnabled) {
await sendScheduledEmailsAndSMS({ ...evt }, undefined, undefined, undefined, eventType.metadata);
const eventTypeMetadata = EventTypeMetaDataSchema.parse(eventType?.metadata || {});

// For seated events, send emails only to the attendee who just completed payment
if (evt.seatsPerTimeSlot) {
// Find the attendee whose bookingSeat has this paymentId
const attendeeWhoPaid = evt.attendees.find((a) => a.bookingSeat?.paymentId === paymentId);

if (attendeeWhoPaid) {
const workflows = await getAllWorkflowsFromEventType(booking.eventType, booking.userId);

let isHostConfirmationEmailsDisabled = false;
let isAttendeeConfirmationEmailDisabled = false;

if (workflows) {
isHostConfirmationEmailsDisabled =
eventTypeMetadata?.disableStandardEmails?.confirmation?.host || false;
isAttendeeConfirmationEmailDisabled =
eventTypeMetadata?.disableStandardEmails?.confirmation?.attendee || false;

if (isHostConfirmationEmailsDisabled) {
isHostConfirmationEmailsDisabled = allowDisablingHostConfirmationEmails(workflows);
}

if (isAttendeeConfirmationEmailDisabled) {
isAttendeeConfirmationEmailDisabled = allowDisablingAttendeeConfirmationEmails(workflows);
}
}

const newSeat = evt.attendees.length > 1;

await sendScheduledSeatsEmailsAndSMS(
evt,
attendeeWhoPaid,
newSeat,
!!evt.seatsShowAttendees,
isHostConfirmationEmailsDisabled,
isAttendeeConfirmationEmailDisabled,
eventTypeMetadata
);
}
} else {
await sendScheduledEmailsAndSMS({ ...evt }, undefined, undefined, undefined, eventTypeMetadata);
}
}

throw new HttpCode({
Expand Down
4 changes: 2 additions & 2 deletions packages/emails/email-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ const _sendScheduledEmailsAndSMS = async (
title: getEventName({ ...eventNameObject, t: attendee.language.translate }),
}),
},
attendee
attendee,
formattedCalEvent.seatsShowAttendees ?? false
)
);
})
Expand Down Expand Up @@ -531,7 +532,6 @@ export const sendAwaitingPaymentEmailAndSMS = async (
await awaitingPaymentSMS.sendSMSToAttendees();
};


export const sendRequestRescheduleEmailAndSMS = async (
calEvent: CalendarEvent,
metadata: { rescheduleLink: string },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,10 @@ const createNewSeat = async (
}
const copyEvent = cloneDeep(evt);
copyEvent.uid = seatedBooking.uid;
if (noEmail !== true) {

const requiresPayment = !Number.isNaN(paymentAppData.price) && paymentAppData.price > 0 && !!seatedBooking;

if (noEmail !== true && !requiresPayment) {
let isHostConfirmationEmailsDisabled = false;
let isAttendeeConfirmationEmailDisabled = false;

Expand Down Expand Up @@ -158,7 +161,7 @@ const createNewSeat = async (

const foundBooking = await findBookingQuery(seatedBooking.id);

if (!Number.isNaN(paymentAppData.price) && paymentAppData.price > 0 && !!seatedBooking) {
if (requiresPayment) {
const credentialPaymentAppCategories = await prisma.credential.findMany({
where: {
...(paymentAppData.credentialId ? { id: paymentAppData.credentialId } : { userId: organizerUser.id }),
Expand Down Expand Up @@ -209,6 +212,13 @@ const createNewSeat = async (
bookerPhoneNumber,
});

if (payment?.id && newBookingSeat?.id) {
await prisma.bookingSeat.update({
where: { id: newBookingSeat.id },
data: { paymentId: payment.id },
});
}

resultBooking = { ...foundBooking };
resultBooking["message"] = "Payment required";
resultBooking["paymentUid"] = payment?.uid;
Expand Down
25 changes: 25 additions & 0 deletions packages/features/bookings/lib/payment/getBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ async function getEventType(id: number) {
recurringEvent: true,
requiresConfirmation: true,
metadata: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
},
});
}
Expand All @@ -33,6 +35,26 @@ export async function getBooking(bookingId: number) {
select: {
...bookingMinimalSelect,
responses: true,
attendees: {
select: {
id: true,
name: true,
email: true,
timeZone: true,
locale: true,
bookingSeat: {
select: {
id: true,
referenceUid: true,
data: true,
metadata: true,
bookingId: true,
attendeeId: true,
paymentId: true,
},
},
},
},
eventType: {
select: {
owner: {
Expand Down Expand Up @@ -142,6 +164,7 @@ export async function getBooking(bookingId: number) {
translate: await getTranslation(attendee.locale ?? "en", "common"),
locale: attendee.locale ?? "en",
},
bookingSeat: attendee.bookingSeat,
};
});

Expand Down Expand Up @@ -195,6 +218,8 @@ export async function getBooking(bookingId: number) {
destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [],
recurringEvent: parseRecurringEvent(eventType?.recurringEvent),
customReplyToEmail: booking.eventType?.customReplyToEmail,
seatsPerTimeSlot: eventType?.seatsPerTimeSlot,
seatsShowAttendees: eventType?.seatsShowAttendees,
};

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1802,7 +1802,7 @@ async function handler(

// Save description to bookingSeat
const uniqueAttendeeId = uuid();
await deps.prismaClient.bookingSeat.create({
const newBookingSeat = await deps.prismaClient.bookingSeat.create({
data: {
referenceUid: uniqueAttendeeId,
data: {
Expand All @@ -1821,8 +1821,37 @@ async function handler(
},
},
},
select: {
id: true,
referenceUid: true,
data: true,
metadata: true,
bookingId: true,
attendeeId: true,
},
});
evt.attendeeSeatId = uniqueAttendeeId;

evt = {
...evt,
attendees: evt.attendees.map((attendee) => {
if (attendee.email === bookerEmail) {
return {
...attendee,
bookingSeat: {
id: newBookingSeat.id,
referenceUid: newBookingSeat.referenceUid,
data: newBookingSeat.data,
metadata: newBookingSeat.metadata,
bookingId: newBookingSeat.bookingId,
attendeeId: newBookingSeat.attendeeId,
paymentId: null,
},
};
}
return attendee;
}),
};
}
} else {
const { booking: dryRunBooking, troubleshooterData: _troubleshooterData } = buildDryRunBooking({
Expand Down Expand Up @@ -2347,6 +2376,14 @@ async function handler(
bookingFields: eventType.bookingFields,
locale: language,
});

if (payment?.id && evt.attendeeSeatId) {
await deps.prismaClient.bookingSeat.update({
where: { referenceUid: evt.attendeeSeatId },
data: { paymentId: payment.id },
});
}

const subscriberOptionsPaymentInitiated: GetSubscriberOptions = {
userId: triggerForUser ? organizerUser.id : null,
eventTypeId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
Warnings:
- A unique constraint covering the columns `[paymentId]` on the table `BookingSeat` will be added. If there are existing duplicate values, this will fail.
Comment on lines +1 to +4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check this wanings

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This warning is normal and expected when adding a unique constraint. Since paymentId is a new column being added by this migration, there are no existing values in the database to conflict - all existing rows will have paymentId = NULL, which is allowed for unique constraints.

The migration will run successfully without any issues. This is just Prisma's standard safety reminder that appears whenever adding a unique constraint to warn about potential duplicate data (which doesn't apply in our case).

*/
-- AlterTable
ALTER TABLE "public"."BookingSeat" ADD COLUMN "paymentId" INTEGER;

-- CreateIndex
CREATE UNIQUE INDEX "BookingSeat_paymentId_key" ON "public"."BookingSeat"("paymentId");
3 changes: 2 additions & 1 deletion packages/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -1626,6 +1626,7 @@ model BookingSeat {
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
attendeeId Int @unique
attendee Attendee @relation(fields: [attendeeId], references: [id], onDelete: Cascade)
paymentId Int? @unique
/// @zod.import(["import { bookingSeatDataSchema } from '../../zod-utils'"]).custom.use(bookingSeatDataSchema)
data Json?
metadata Json?
Expand Down Expand Up @@ -2893,4 +2894,4 @@ model CalendarCacheEvent {
@@unique([selectedCalendarId, externalId])
@@index([start, end, status])
@@index([selectedCalendarId, iCalUID])
}
}
Loading