* Add credits section to billing * create seperate router for credits * add stripe checkout session * schema changes + code improvements * rename to creditBalance * custom quantify input with error message * add checkout session completed webhook endpoint * fix typo * UI fixes * add payCredits handler * add error toast message * allow scheduling sms up as close to 15 minutes in the future * schedule at most 2 hours in advance * webhook to pay for sent sms * continued work on twilio callback * code clean up * further implementation for credit handling * add migration * object as param for scheduleSMS * object as param for sendSMS * fix TrpcSessionUser imports * fix imports * add db changes * add cron job for price setting * twilio status callback to create expense log * remove unused code * set up low credit balance email * fixes for buying credits * fixes in api/twilio/webhook * add test to save credits to credits balance * fix typos * add new helper function chargeCredits * expand twilioProvider * fix type errors * adjust tests * type errors * clean up * clean up * fix subscription active check * remove some user/org related code * more changes to remove user/org support * send emails seperatly to admins * fixes for team billing page * fix stripe success url * fixes to creating expense log * email imrovements and more * get monthly team price from stripe * fix import * fix monthly credits calculation * finsih low credit balance warning email * credit balance limit reached email * create CreditService * cancel SMS and send as email instead * add messageDispatcher * fix type error * fix type error * fix type error * fix import * fix unit test * clean up twilioProvider * clean up chckSmsPrices/route * add missing translations * add skeleton loader * add admin check to get handler * code clean up + fixes * improve scheduling with fallback * fix type error * add bookingUid to handleSendingSMS * add unit tests for creditService * add more tests to credit-service.test.ts * add test for cancelScheduledMessagesAndScheduleEmails * fix test and type error * add back resolve * fix empty resolve * adjust limitReachedAt logic * address mrge comment on styling * add getAdminMembership to repository * twilio/webhook clean up (feedback) * feedback - clean up * remove todo comment * clean up twilio/webhook * code clean up * add use client * add createOneTimeCheckout to stripe service * refactor repository pattern * small fixes + clean up * fix type error * add missing import * fix hasAvailableCredits for user * force-dynamic * rename credits to creditBalance * fix stripe import * remove not needed code * fix e2e tests * improve low balance warning email * dynamic-import CreditService * index.ts * fix e2e tests * remove dynamic import CreditService * Revert "remove dynamic import CreditService" This reverts commit e272978a7ff3fc5a04139e656c9f8d2c84a40dda. * no need to dynamic-import credit service * Revert "no need to dynamic-import credit service" This reverts commit ba5ae488d08979a65fb47b5d0722cda9f45d6ea0. * only select id in getAdminMembership * revert billing/package.json * fix type checks * fix type checks --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: hbjORbj <sldisek783@gmail.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com>
120 lines
3.4 KiB
TypeScript
120 lines
3.4 KiB
TypeScript
import dayjs from "@calcom/dayjs";
|
|
import { getSenderId } from "@calcom/features/ee/workflows/lib/alphanumericSenderIdSupport";
|
|
import { sendSmsOrFallbackEmail } from "@calcom/features/ee/workflows/lib/reminders/messageDispatcher";
|
|
import { checkSMSRateLimit } from "@calcom/lib/checkRateLimitAndThrowError";
|
|
import { SENDER_ID } from "@calcom/lib/constants";
|
|
import isSmsCalEmail from "@calcom/lib/isSmsCalEmail";
|
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
|
import prisma from "@calcom/prisma";
|
|
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
|
|
|
const handleSendingSMS = async ({
|
|
reminderPhone,
|
|
smsMessage,
|
|
senderID,
|
|
teamId,
|
|
bookingUid,
|
|
}: {
|
|
reminderPhone: string;
|
|
smsMessage: string;
|
|
senderID: string;
|
|
teamId: number;
|
|
bookingUid?: string | null;
|
|
}) => {
|
|
const team = await prisma.team.findUnique({
|
|
where: { id: teamId },
|
|
select: {
|
|
parent: {
|
|
select: {
|
|
isOrganization: true,
|
|
organizationSettings: {
|
|
select: {
|
|
disablePhoneOnlySMSNotifications: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!team?.parent?.isOrganization || team?.parent?.organizationSettings?.disablePhoneOnlySMSNotifications) {
|
|
return; // resolves implicitly (as undefined)
|
|
}
|
|
|
|
try {
|
|
await checkSMSRateLimit({
|
|
identifier: `handleSendingSMS:team:${teamId}`,
|
|
rateLimitingType: "sms",
|
|
});
|
|
|
|
const smsOrFallbackEmail = await sendSmsOrFallbackEmail({
|
|
twilioData: {
|
|
phoneNumber: reminderPhone,
|
|
body: smsMessage,
|
|
sender: senderID,
|
|
teamId,
|
|
bookingUid,
|
|
},
|
|
});
|
|
|
|
return smsOrFallbackEmail;
|
|
} catch (e) {
|
|
console.error("sendSmsOrFallbackEmail failed", e);
|
|
throw e; // propagate the error
|
|
}
|
|
};
|
|
|
|
export default abstract class SMSManager {
|
|
calEvent: CalendarEvent;
|
|
isTeamEvent = false;
|
|
teamId: number | undefined = undefined;
|
|
|
|
constructor(calEvent: CalendarEvent) {
|
|
this.calEvent = calEvent;
|
|
this.teamId = this.calEvent?.team?.id;
|
|
this.isTeamEvent = !!this.calEvent?.team?.id;
|
|
}
|
|
|
|
getFormattedTime(
|
|
timezone: string,
|
|
locale: string,
|
|
time: string,
|
|
format = `dddd, LL | ${TimeFormat.TWELVE_HOUR}`
|
|
) {
|
|
return dayjs(time).tz(timezone).locale(locale).format(format);
|
|
}
|
|
|
|
getFormattedDate(timezone: string, locale: string) {
|
|
return `${this.getFormattedTime(timezone, locale, this.calEvent.startTime)} - ${this.getFormattedTime(
|
|
timezone,
|
|
locale,
|
|
this.calEvent.endTime
|
|
)} (${timezone})`;
|
|
}
|
|
|
|
abstract getMessage(attendee: Person): string;
|
|
|
|
async sendSMSToAttendee(attendee: Person, bookingUid?: string | null) {
|
|
const teamId = this.teamId;
|
|
const attendeePhoneNumber = attendee.phoneNumber;
|
|
const isPhoneOnlyBooking = attendeePhoneNumber && isSmsCalEmail(attendee.email);
|
|
|
|
if (!this.isTeamEvent || !teamId || !attendeePhoneNumber || !isPhoneOnlyBooking) return;
|
|
|
|
const smsMessage = this.getMessage(attendee);
|
|
const senderID = getSenderId(attendeePhoneNumber, SENDER_ID);
|
|
return handleSendingSMS({ reminderPhone: attendeePhoneNumber, smsMessage, senderID, teamId, bookingUid });
|
|
}
|
|
|
|
async sendSMSToAttendees() {
|
|
if (!this.isTeamEvent) return;
|
|
const smsToSend: Promise<unknown>[] = [];
|
|
|
|
for (const attendee of this.calEvent.attendees) {
|
|
smsToSend.push(this.sendSMSToAttendee(attendee, this.calEvent.uid));
|
|
}
|
|
|
|
await Promise.all(smsToSend);
|
|
}
|
|
}
|