Files
calendar/packages/sms/sms-manager.ts
T
fadfba8830 feat: Credit System (SMS) (#20126)
* 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>
2025-05-13 09:11:44 +02:00

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);
}
}