* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main: - Rebrand Cal.com to Cal.diy across the entire codebase - Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions - Switch license from AGPL-3.0 to MIT - Remove docs/ directory (migrated to Nextra at cal.diy) - Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc. - Clean up .env.example for self-hosted Cal.diy - Update Docker image references to calcom/cal.diy - Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork - Add PR welcome bot for Cal.diy contributors - Fix API v2 breaking changes oasdiff ignore entries - Replace Blacksmith CI runners with default GitHub Actions 3893 files changed, 20789 insertions(+), 411020 deletions(-) Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * rip out org related comments in api v2 --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
104 lines
3.8 KiB
TypeScript
104 lines
3.8 KiB
TypeScript
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
|
import { sendAwaitingPaymentEmailAndSMS } from "@calcom/emails/email-manager";
|
|
import { getBooking } from "@calcom/features/bookings/lib/payment/getBooking";
|
|
import { AttendeeRepository } from "@calcom/features/bookings/repositories/AttendeeRepository";
|
|
import { PrismaBookingPaymentRepository } from "@calcom/features/bookings/repositories/PrismaBookingPaymentRepository";
|
|
import logger from "@calcom/lib/logger";
|
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
|
import prisma from "@calcom/prisma";
|
|
import { z } from "zod";
|
|
|
|
const log = logger.getSubLogger({ prefix: ["sendAwaitingPaymentEmail"] });
|
|
|
|
export const sendAwaitingPaymentEmailPayloadSchema = z.object({
|
|
bookingId: z.number(),
|
|
paymentId: z.number(),
|
|
attendeeSeatId: z.string().nullable().optional(),
|
|
});
|
|
|
|
export async function sendAwaitingPaymentEmail(payload: string): Promise<void> {
|
|
const paymentRepository = new PrismaBookingPaymentRepository();
|
|
|
|
try {
|
|
const { bookingId, paymentId, attendeeSeatId } = sendAwaitingPaymentEmailPayloadSchema.parse(
|
|
JSON.parse(payload)
|
|
);
|
|
|
|
log.debug(`Processing sendAwaitingPaymentEmail task for bookingId ${bookingId}, paymentId ${paymentId}`);
|
|
|
|
const { booking, evt, eventType } = await getBooking(bookingId);
|
|
|
|
const payment = await paymentRepository.findByIdForAwaitingPaymentEmail(paymentId);
|
|
|
|
if (!payment) {
|
|
log.warn(`Payment ${paymentId} not found, skipping email`);
|
|
return;
|
|
}
|
|
|
|
if (payment.success || booking.paid) {
|
|
log.debug(
|
|
`Payment ${paymentId} already succeeded or booking ${bookingId} already paid, skipping email`
|
|
);
|
|
return;
|
|
}
|
|
|
|
// filter attendees if this is for a specific seat
|
|
let attendeesToEmail = evt.attendees;
|
|
if (attendeeSeatId) {
|
|
const attendeeRepository = new AttendeeRepository(prisma);
|
|
const seatAttendees = await attendeeRepository.findByBookingIdAndSeatReference({
|
|
bookingId,
|
|
seatReferenceUid: attendeeSeatId,
|
|
});
|
|
const seatEmails = new Set(seatAttendees.map((a) => (a.email || "").toLowerCase()));
|
|
attendeesToEmail = evt.attendees.filter((attendee) =>
|
|
seatEmails.has((attendee.email || "").toLowerCase())
|
|
);
|
|
|
|
if (attendeesToEmail.length === 0) {
|
|
log.warn(`No attendees found for seat ${attendeeSeatId} in booking ${bookingId}, skipping email`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/*
|
|
the reason why we use the first attendee's info for the payment link is because:
|
|
1. for regular bookings: the first attendee in the array is typically the booker (the person who made the booking and is responsible for payment)
|
|
2. for seated events: after filtering by attendeeSeatId, there's usually only one attendee anyway
|
|
*/
|
|
const primaryAttendee = attendeesToEmail[0];
|
|
|
|
if (!primaryAttendee) {
|
|
log.warn(`No attendees found for booking ${bookingId}, skipping email`);
|
|
return;
|
|
}
|
|
|
|
await sendAwaitingPaymentEmailAndSMS(
|
|
{
|
|
...evt,
|
|
attendees: attendeesToEmail,
|
|
paymentInfo: {
|
|
link: createPaymentLink({
|
|
paymentUid: payment.uid,
|
|
name: primaryAttendee.name ?? null,
|
|
email: primaryAttendee.email ?? null,
|
|
date: booking.startTime.toISOString(),
|
|
}),
|
|
paymentOption: payment.paymentOption || "ON_BOOKING",
|
|
amount: payment.amount,
|
|
currency: payment.currency,
|
|
},
|
|
},
|
|
eventType.metadata
|
|
);
|
|
|
|
log.debug(`Successfully sent awaiting payment email for bookingId ${bookingId}`);
|
|
} catch (error) {
|
|
log.error(
|
|
`Failed to send awaiting payment email`,
|
|
safeStringify({ payload, error: error instanceof Error ? error.message : String(error) })
|
|
);
|
|
throw error;
|
|
}
|
|
}
|