diff --git a/Common/Types/SmsStatus.ts b/Common/Types/SmsStatus.ts index 14af6822fe..e4f63cb5fb 100644 --- a/Common/Types/SmsStatus.ts +++ b/Common/Types/SmsStatus.ts @@ -1,7 +1,7 @@ enum SmsStatus { Success = 'Success', Error = 'Error', - LowBalance = 'Low Balance', + LowBalance = 'Low Balance', } export default SmsStatus; diff --git a/CommonServer/Services/BillingService.ts b/CommonServer/Services/BillingService.ts index 91b562f0ca..bcc3730ac2 100644 --- a/CommonServer/Services/BillingService.ts +++ b/CommonServer/Services/BillingService.ts @@ -497,6 +497,35 @@ export class BillingService { }); } + public static async genrateInvoiceAndChargeCustomer( + customerId: string, + itemText: string, + amountInUsd: number + ): Promise { + const invoice: Stripe.Invoice = await this.stripe.invoices.create({ + customer: customerId, + auto_advance: true, // do not automatically charge. + collection_method: 'charge_automatically', + }); + + if (!invoice || !invoice.id) { + throw new APIException('Invoice not generated.'); + } + + await this.stripe.invoiceItems.create({ + invoice: invoice.id, + amount: amountInUsd * 100, + quantity: 1, + description: itemText, + currency: 'usd', + customer: customerId, + }); + + await this.stripe.invoices.finalizeInvoice(invoice.id!); + + await this.stripe.invoices.pay(invoice.id); + } + public static async payInvoice( customerId: string, invoiceId: string diff --git a/CommonServer/Services/SmsService.ts b/CommonServer/Services/SmsService.ts index f88b4545f7..c822864c4a 100644 --- a/CommonServer/Services/SmsService.ts +++ b/CommonServer/Services/SmsService.ts @@ -12,12 +12,13 @@ import ObjectID from 'Common/Types/ObjectID'; export default class SmsService { public static async sendSms( - to: Phone, message: string, options: { - projectId?: ObjectID | undefined // project id for sms log - from?: Phone, // from phone number + to: Phone, + message: string, + options: { + projectId?: ObjectID | undefined; // project id for sms log + from?: Phone; // from phone number } ): Promise> { - const body: JSONObject = { to: to.toString(), message, diff --git a/CommonUI/src/Components/Detail/Detail.tsx b/CommonUI/src/Components/Detail/Detail.tsx index 9678f6d3b3..f9fc44aa9d 100644 --- a/CommonUI/src/Components/Detail/Detail.tsx +++ b/CommonUI/src/Components/Detail/Detail.tsx @@ -35,8 +35,6 @@ const Detail: Function = (props: ComponentProps): ReactElement => { return ; }; - - const getDropdownViewer: Function = ( data: string, options: Array, @@ -76,7 +74,7 @@ const Detail: Function = (props: ComponentProps): ReactElement => { }; const getUSDCentsField: Function = (usdCents: number): ReactElement => { - return
{usdCents/100} USD
; + return
{usdCents / 100} USD
; }; const getField: Function = (field: Field, index: number): ReactElement => { @@ -288,5 +286,3 @@ const Detail: Function = (props: ComponentProps): ReactElement => { }; export default Detail; - - diff --git a/Notification/API/SMS.ts b/Notification/API/SMS.ts index 65c29310ae..aed3e53e78 100644 --- a/Notification/API/SMS.ts +++ b/Notification/API/SMS.ts @@ -18,10 +18,14 @@ router.post( async (req: ExpressRequest, res: ExpressResponse) => { const body: JSONObject = JSONFunctions.deserialize(req.body); - await SmsService.sendSms(body['to'] as Phone, body['message'] as string, { - projectId: body['projectId'] as ObjectID, - from: body['from'] as Phone, - }); + await SmsService.sendSms( + body['to'] as Phone, + body['message'] as string, + { + projectId: body['projectId'] as ObjectID, + from: body['from'] as Phone, + } + ); return Response.sendEmptyResponse(req, res); } diff --git a/Notification/Config.ts b/Notification/Config.ts index 5df7eb98ff..cb5174d56b 100644 --- a/Notification/Config.ts +++ b/Notification/Config.ts @@ -28,7 +28,12 @@ export const InternalSmtpFromName: string = export const TwilioAccountSid: string = process.env['TWILIO_ACCOUNT_SID'] || ''; export const TwilioAuthToken: string = process.env['TWILIO_AUTH_TOKEN'] || ''; -export const TwilioPhoneNumber: string = process.env['TWILIO_PHONE_NUMBER'] || ''; -export const SMSDefaultCostInCents: number = process.env['SMS_DEFAULT_COST_IN_CENTS'] ? parseInt(process.env['SMS_DEFAULT_COST_IN_CENTS']) : 0; +export const TwilioPhoneNumber: string = + process.env['TWILIO_PHONE_NUMBER'] || ''; +export const SMSDefaultCostInCents: number = process.env[ + 'SMS_DEFAULT_COST_IN_CENTS' +] + ? parseInt(process.env['SMS_DEFAULT_COST_IN_CENTS']) + : 0; export const SendGridApiKey: string = process.env['SENDGRID_API_KEY'] || ''; diff --git a/Notification/Services/SmsService.ts b/Notification/Services/SmsService.ts index 927c8fa2f9..19510aa174 100644 --- a/Notification/Services/SmsService.ts +++ b/Notification/Services/SmsService.ts @@ -10,6 +10,8 @@ import SmsLogService from "CommonServer/Services/SmsLogService" import ProjectService from "CommonServer/Services/ProjectService"; import Project from "Model/Models/Project"; import { MessageInstance } from "twilio/lib/rest/api/v2010/account/message"; +import BillingService from "CommonServer/Services/BillingService"; +import logger from "CommonServer/Utils/Logger"; export default class SmsService { public static async sendSms(to: Phone, message: string, options: { @@ -48,12 +50,16 @@ export default class SmsService { // make sure project has enough balance. if (options.projectId && IsBillingEnabled) { + project = await ProjectService.findOneById({ id: options.projectId, select: { smsOrCallCurrentBalanceInUSDCents: true, enableAutoRechargeSmsOrCallBalance: true, - enableSmsNotifications: true + enableSmsNotifications: true, + autoRechargeSmsOrCallByBalanceInUSD: true, + autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: true, + paymentProviderCustomerId: true }, props: { isRoot: true @@ -63,7 +69,7 @@ export default class SmsService { if (!project) { smsLog.status = SmsStatus.Error; smsLog.statusMessage = `Project ${options.projectId.toString()} not found.`; - await SmsLogService.create({ + await SmsLogService.create({ data: smsLog, props: { isRoot: true @@ -73,7 +79,7 @@ export default class SmsService { } - if(!project.enableSmsNotifications){ + if (!project.enableSmsNotifications) { smsLog.status = SmsStatus.Error; smsLog.statusMessage = `SMS notifications are not enabled for this project. Please enable SMS notifications in project settings.`; await SmsLogService.create({ @@ -85,6 +91,39 @@ export default class SmsService { return; } + // check if auto recharge is enabled and current balance is low. + + if (IsBillingEnabled && project && project.enableAutoRechargeSmsOrCallBalance && project.autoRechargeSmsOrCallByBalanceInUSD && project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD) { + + if (project.smsOrCallCurrentBalanceInUSDCents && project.smsOrCallCurrentBalanceInUSDCents < project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD) { + try { + // recharge balance + const updatedAmount: number = Math.floor(project.smsOrCallCurrentBalanceInUSDCents + (project.autoRechargeSmsOrCallByBalanceInUSD * 100)); + + // If the recharge is succcessful, then update the project balance. + await BillingService.genrateInvoiceAndChargeCustomer(project.paymentProviderCustomerId!, "SMS or Call Balance Recharge", project.autoRechargeSmsOrCallByBalanceInUSD); + + await ProjectService.updateOneById({ + data: { + smsOrCallCurrentBalanceInUSDCents: updatedAmount + }, + id: project.id!, + props: { + isRoot: true + } + }); + + project.smsOrCallCurrentBalanceInUSDCents = updatedAmount; + + // TODO: Send an email on successful recharge. + } catch (err) { + // TODO: if the recharge fails, then send email to the user. + logger.error(err); + } + + } + } + if (!project.smsOrCallCurrentBalanceInUSDCents) { smsLog.status = SmsStatus.LowBalance; smsLog.statusMessage = `Project ${options.projectId.toString()} does not have enough SMS balance.`; @@ -96,7 +135,7 @@ export default class SmsService { }); return; - + } if (project.smsOrCallCurrentBalanceInUSDCents < SMSDefaultCostInCents) { @@ -109,7 +148,6 @@ export default class SmsService { } }); return; - } } @@ -153,6 +191,9 @@ export default class SmsService { isRoot: true } }); + + + } } } \ No newline at end of file