import { SMSDefaultCostInCents, SMSHighRiskCostInCents, getTwilioConfig, } from "../Config"; import { isHighRiskPhoneNumber } from "Common/Types/Call/CallRequest"; import TwilioConfig from "Common/Types/CallAndSMS/TwilioConfig"; import BadDataException from "Common/Types/Exception/BadDataException"; import ObjectID from "Common/Types/ObjectID"; import Phone from "Common/Types/Phone"; import SmsStatus from "Common/Types/SmsStatus"; import Text from "Common/Types/Text"; import UserNotificationStatus from "Common/Types/UserNotification/UserNotificationStatus"; import { IsBillingEnabled } from "CommonServer/EnvironmentConfig"; import NotificationService from "CommonServer/Services/NotificationService"; import ProjectService from "CommonServer/Services/ProjectService"; import SmsLogService from "CommonServer/Services/SmsLogService"; import UserOnCallLogTimelineService from "CommonServer/Services/UserOnCallLogTimelineService"; import logger from "CommonServer/Utils/Logger"; import Project from "Model/Models/Project"; import SmsLog from "Model/Models/SmsLog"; import Twilio from "twilio"; import { MessageInstance } from "twilio/lib/rest/api/v2010/account/message"; export default class SmsService { public static async sendSms( to: Phone, message: string, options: { projectId?: ObjectID | undefined; // project id for sms log customTwilioConfig?: TwilioConfig | undefined; isSensitive?: boolean; // if true, message will not be logged userOnCallLogTimelineId?: ObjectID | undefined; }, ): Promise { let smsError: Error | null = null; const smsLog: SmsLog = new SmsLog(); try { // check number of sms to send for this entire messages to send. Each sms can have 160 characters. const smsSegments: number = Math.ceil(message.length / 160); message = Text.trimLines(message); let smsCost: number = 0; const shouldChargeForSMS: boolean = IsBillingEnabled && !options.customTwilioConfig; if (shouldChargeForSMS) { smsCost = SMSDefaultCostInCents / 100; if (isHighRiskPhoneNumber(to)) { smsCost = SMSHighRiskCostInCents / 100; } } if (smsSegments > 1) { smsCost = smsCost * smsSegments; } smsLog.toNumber = to; smsLog.smsText = options && options.isSensitive ? "This message is sensitive and is not logged" : message; smsLog.smsCostInUSDCents = 0; if (options.projectId) { smsLog.projectId = options.projectId; } const twilioConfig: TwilioConfig | null = options.customTwilioConfig || (await getTwilioConfig()); if (!twilioConfig) { throw new BadDataException("Twilio Config not found"); } const client: Twilio.Twilio = Twilio( twilioConfig.accountSid, twilioConfig.authToken, ); smsLog.fromNumber = twilioConfig.phoneNumber; let project: Project | null = null; // make sure project has enough balance. if (options.projectId) { project = await ProjectService.findOneById({ id: options.projectId, select: { smsOrCallCurrentBalanceInUSDCents: true, enableSmsNotifications: true, lowCallAndSMSBalanceNotificationSentToOwners: true, name: true, notEnabledSmsOrCallNotificationSentToOwners: true, }, props: { isRoot: true, }, }); if (!project) { smsLog.status = SmsStatus.Error; smsLog.statusMessage = `Project ${options.projectId.toString()} not found.`; logger.error(smsLog.statusMessage); await SmsLogService.create({ data: smsLog, props: { isRoot: true, }, }); return; } if (!project.enableSmsNotifications) { smsLog.status = SmsStatus.Error; smsLog.statusMessage = `SMS notifications are not enabled for this project. Please enable SMS notifications in Project Settings.`; logger.error(smsLog.statusMessage); await SmsLogService.create({ data: smsLog, props: { isRoot: true, }, }); if (!project.notEnabledSmsOrCallNotificationSentToOwners) { await ProjectService.updateOneById({ data: { notEnabledSmsOrCallNotificationSentToOwners: true, }, id: project.id!, props: { isRoot: true, }, }); await ProjectService.sendEmailToProjectOwners( project.id!, "SMS notifications not enabled for " + (project.name || ""), `We tried to send an SMS to ${to.toString()} with message:

${message}

This SMS was not sent because SMS notifications are not enabled for this project. Please enable SMS notifications in Project Settings.`, ); } return; } if (shouldChargeForSMS) { // check if auto recharge is enabled and current balance is low. let updatedBalance: number = project.smsOrCallCurrentBalanceInUSDCents!; try { updatedBalance = await NotificationService.rechargeIfBalanceIsLow( project.id!, ); } catch (err) { logger.error(err); } project.smsOrCallCurrentBalanceInUSDCents = updatedBalance; if (!project.smsOrCallCurrentBalanceInUSDCents) { smsLog.status = SmsStatus.LowBalance; smsLog.statusMessage = `Project ${options.projectId.toString()} does not have enough SMS balance.`; logger.error(smsLog.statusMessage); await SmsLogService.create({ data: smsLog, props: { isRoot: true, }, }); if (!project.lowCallAndSMSBalanceNotificationSentToOwners) { await ProjectService.updateOneById({ data: { lowCallAndSMSBalanceNotificationSentToOwners: true, }, id: project.id!, props: { isRoot: true, }, }); await ProjectService.sendEmailToProjectOwners( project.id!, "Low SMS and Call Balance for " + (project.name || ""), `We tried to send an SMS to ${to.toString()} with message:

${message}
This SMS was not sent because project does not have enough balance to send SMS. Current balance is ${ (project.smsOrCallCurrentBalanceInUSDCents || 0) / 100 } USD cents. Required balance to send this SMS should is ${smsCost} USD. Please enable auto recharge or recharge manually.`, ); } return; } if (project.smsOrCallCurrentBalanceInUSDCents < smsCost * 100) { smsLog.status = SmsStatus.LowBalance; smsLog.statusMessage = `Project does not have enough balance to send SMS. Current balance is ${ project.smsOrCallCurrentBalanceInUSDCents / 100 } USD. Required balance is ${smsCost} USD to send this SMS.`; logger.error(smsLog.statusMessage); await SmsLogService.create({ data: smsLog, props: { isRoot: true, }, }); if (!project.lowCallAndSMSBalanceNotificationSentToOwners) { await ProjectService.updateOneById({ data: { lowCallAndSMSBalanceNotificationSentToOwners: true, }, id: project.id!, props: { isRoot: true, }, }); await ProjectService.sendEmailToProjectOwners( project.id!, "Low SMS and Call Balance for " + (project.name || ""), `We tried to send an SMS to ${to.toString()} with message:

${message}

This SMS was not sent because project does not have enough balance to send SMS. Current balance is ${ project.smsOrCallCurrentBalanceInUSDCents / 100 } USD. Required balance is ${smsCost} USD to send this SMS. Please enable auto recharge or recharge manually.`, ); } return; } } } const twillioMessage: MessageInstance = await client.messages.create({ body: message, to: to.toString(), from: twilioConfig.phoneNumber.toString(), // From a valid Twilio number }); smsLog.status = SmsStatus.Success; smsLog.statusMessage = "Message ID: " + twillioMessage.sid; logger.debug("SMS message sent successfully."); logger.debug(smsLog.statusMessage); if (shouldChargeForSMS && project) { smsLog.smsCostInUSDCents = smsCost * 100; project.smsOrCallCurrentBalanceInUSDCents = Math.floor( project.smsOrCallCurrentBalanceInUSDCents! - smsCost * 100, ); await ProjectService.updateOneById({ data: { smsOrCallCurrentBalanceInUSDCents: project.smsOrCallCurrentBalanceInUSDCents, notEnabledSmsOrCallNotificationSentToOwners: false, // reset this flag }, id: project.id!, props: { isRoot: true, }, }); } } catch (e: any) { smsLog.smsCostInUSDCents = 0; smsLog.status = SmsStatus.Error; smsLog.statusMessage = e && e.message ? e.message.toString() : e.toString(); logger.error("SMS message failed to send."); logger.error(smsLog.statusMessage); smsError = e; } if (options.projectId) { await SmsLogService.create({ data: smsLog, props: { isRoot: true, }, }); } if (options.userOnCallLogTimelineId) { await UserOnCallLogTimelineService.updateOneById({ data: { status: smsLog.status === SmsStatus.Success ? UserNotificationStatus.Sent : UserNotificationStatus.Error, statusMessage: smsLog.statusMessage!, }, id: options.userOnCallLogTimelineId, props: { isRoot: true, }, }); } if (smsError) { throw smsError; } } }