From 1ed51a6dc27867ee26355fecfe228a4a74d0e87f Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Fri, 9 Jun 2023 13:39:20 +0100 Subject: [PATCH] implement emails for erorr states --- Common/Types/API/Hostname.ts | 2 +- Common/Types/Email/EmailTemplateType.ts | 1 + CommonServer/Services/BillingService.ts | 16 ++++- CommonServer/Services/NotificationService.ts | 36 +++++++++++- CommonServer/Services/ProjectService.ts | 39 ++++++++++-- Model/Models/Project.ts | 62 ++++++++++++++++++++ Notification/Services/SmsService.ts | 47 ++++++++++++++- Notification/Templates/SimpleMessage.hbs | 11 ++++ 8 files changed, 204 insertions(+), 10 deletions(-) create mode 100644 Notification/Templates/SimpleMessage.hbs diff --git a/Common/Types/API/Hostname.ts b/Common/Types/API/Hostname.ts index d7acc122a1..b16e476214 100644 --- a/Common/Types/API/Hostname.ts +++ b/Common/Types/API/Hostname.ts @@ -23,7 +23,7 @@ export default class Hostname extends DatabaseProperty { if (Hostname.isValid(value)) { this._route = value; } else { - throw new BadDataException('Hostname is not in valid format.'); + throw new BadDataException('Hostname '+value+' is not in valid format.'); } } diff --git a/Common/Types/Email/EmailTemplateType.ts b/Common/Types/Email/EmailTemplateType.ts index 626fc30b97..9f0412384b 100644 --- a/Common/Types/Email/EmailTemplateType.ts +++ b/Common/Types/Email/EmailTemplateType.ts @@ -31,6 +31,7 @@ enum EmailTemplateType { StatusPageOwnerResourceCreated = 'StatusPageOwnerResourceCreated.hbs', StatusPageOwnerAdded = 'StatusPageOwnerAdded.hbs', StatusPageOwnerAnnouncementPosted = 'StatusPageOwnerAnnouncementPosted.hbs', + SimpleMessage = 'SimpleMessage.hbs', } export default EmailTemplateType; diff --git a/CommonServer/Services/BillingService.ts b/CommonServer/Services/BillingService.ts index 67127a0b42..726e6ab626 100644 --- a/CommonServer/Services/BillingService.ts +++ b/CommonServer/Services/BillingService.ts @@ -531,7 +531,21 @@ export class BillingService { await this.stripe.invoices.finalizeInvoice(invoice.id!); - await this.payInvoice(customerId, invoice.id!); + try { + await this.payInvoice(customerId, invoice.id!); + } catch (err) { + // mark invoice as failed and do not collect payment. + await this.voidInvoice(invoice.id!); + throw err; + } + } + + public static async voidInvoice(invoiceId: string): Promise { + const invoice = await this.stripe.invoices.voidInvoice( + invoiceId + ); + + return invoice; } public static async payInvoice( diff --git a/CommonServer/Services/NotificationService.ts b/CommonServer/Services/NotificationService.ts index 498e6a27bc..395bb700f0 100644 --- a/CommonServer/Services/NotificationService.ts +++ b/CommonServer/Services/NotificationService.ts @@ -28,6 +28,8 @@ export default class NotificationService { autoRechargeSmsOrCallByBalanceInUSD: true, autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: true, paymentProviderCustomerId: true, + name: true, + failedCallAndSMSBalanceChargeNotificationSentToOwners: true }, props: { isRoot: true, @@ -56,6 +58,19 @@ export default class NotificationService { project.paymentProviderCustomerId! )) ) { + if (!project.failedCallAndSMSBalanceChargeNotificationSentToOwners) { + await ProjectService.updateOneById({ + data: { + failedCallAndSMSBalanceChargeNotificationSentToOwners: + true, + }, + id: project.id!, + props: { + isRoot: true, + }, + }); + await ProjectService.sendEmailToProjectOwners(project.id!, "ACTION REQUIRED: SMS and Call Recharge Failed for project - " + (project.name || ''), `We have tried recharged your SMS and Call balance for project - ${project.name || ''} and failed. We could not find a payment method for the project. Please add a payment method in project settings.`); + } throw new BadDataException( 'No payment methods found for the project. Please add a payment method in project settings to continue.' ); @@ -74,7 +89,7 @@ export default class NotificationService { // recharge balance const updatedAmount: number = Math.floor( (project.smsOrCallCurrentBalanceInUSDCents || 0) + - autoRechargeSmsOrCallByBalanceInUSD * 100 + autoRechargeSmsOrCallByBalanceInUSD * 100 ); // If the recharge is succcessful, then update the project balance. @@ -88,6 +103,9 @@ export default class NotificationService { data: { smsOrCallCurrentBalanceInUSDCents: updatedAmount, + failedCallAndSMSBalanceChargeNotificationSentToOwners: false, // reset this flag + lowCallAndSMSBalanceNotificationSentToOwners: false, // reset this flag + notEnabledSmsNotificationSentToOwners: false }, id: project.id!, props: { @@ -95,12 +113,24 @@ export default class NotificationService { }, }); + await ProjectService.sendEmailToProjectOwners(project.id!, "SMS and Call Recharge Successful for project - " + (project.name || ''), `We have successfully recharged your SMS and Call balance for project - ${project.name || ''} by ${autoRechargeSmsOrCallByBalanceInUSD} USD. Your current balance is ${updatedAmount / 100} USD.`); + project.smsOrCallCurrentBalanceInUSDCents = updatedAmount; - // TODO: Send an email on successful recharge. + } catch (err) { - // TODO: if the recharge fails, then send email to the user. + await ProjectService.updateOneById({ + data: { + failedCallAndSMSBalanceChargeNotificationSentToOwners: + true, + }, + id: project.id!, + props: { + isRoot: true, + }, + }); + await ProjectService.sendEmailToProjectOwners(project.id!, "ACTION REQUIRED: SMS and Call Recharge Failed for project - " + (project.name || ''), `We have tried recharged your SMS and Call balance for project - ${project.name || ''} and failed. Please make sure your payment method is upto date and has sufficient balance. You can add new payment methods in project settings.`); logger.error(err); } } diff --git a/CommonServer/Services/ProjectService.ts b/CommonServer/Services/ProjectService.ts index 8816e0bfe4..b3d246ae5c 100755 --- a/CommonServer/Services/ProjectService.ts +++ b/CommonServer/Services/ProjectService.ts @@ -43,6 +43,10 @@ import AccessTokenService from './AccessTokenService'; import SubscriptionStatus from 'Common/Types/Billing/SubscriptionStatus'; import User from 'Model/Models/User'; import NotificationService from './NotificationService'; +import MailService from './MailService'; +import logger from '../Utils/Logger'; +import Email from 'Common/Types/Email'; +import EmailTemplateType from 'Common/Types/Email/EmailTemplateType'; export class Service extends DatabaseService { public constructor(postgresDatabase?: PostgresDatabase) { @@ -185,7 +189,7 @@ export class Service extends DatabaseService { plan, project.paymentProviderSubscriptionSeats as number, plan.getYearlyPlanId() === - updateBy.data.paymentProviderPlanId, + updateBy.data.paymentProviderPlanId, project.trialEndsAt ); @@ -724,10 +728,37 @@ export class Service extends DatabaseService { plan === PlanSelect.Free ? false : SubscriptionPlan.isUnpaid( - project.paymentProviderSubscriptionStatus || - SubscriptionStatus.Active - ), + project.paymentProviderSubscriptionStatus || + SubscriptionStatus.Active + ), }; } + + public async sendEmailToProjectOwners(projectId: ObjectID, subject: string, message: string): Promise { + const owners: Array = await this.getOwners(projectId); + + if (owners.length === 0) { + return; + } + + const emails: Array = owners.map((owner: User) => { + return owner.email! + }); + + for(const email of emails) { + MailService.sendMail({ + toEmail: email, + templateType: EmailTemplateType.SimpleMessage, + vars: { + subject: subject, + message: message, + }, + subject: subject, + }).catch((err: Error) => { + logger.error(err); + }); + } + } + } export default new Service(); diff --git a/Model/Models/Project.ts b/Model/Models/Project.ts index e33615abf1..f8bc24be31 100644 --- a/Model/Models/Project.ts +++ b/Model/Models/Project.ts @@ -636,4 +636,66 @@ export default class Model extends TenantModel { type: ColumnType.Boolean, }) public enableAutoRechargeSmsOrCallBalance?: boolean = undefined; + + + @ColumnAccessControl({ + create: [], + read: [], + update: [], + }) + @TableColumn({ + required: true, + isDefaultValueColumn: true, + type: TableColumnType.Boolean, + title: 'Low Call and SMS Balance Notification Sent to Owners', + description: + 'Low Call and SMS Balance Notification Sent to Owners', + }) + @Column({ + nullable: false, + default: false, + type: ColumnType.Boolean, + }) + public lowCallAndSMSBalanceNotificationSentToOwners?: boolean = undefined; + + @ColumnAccessControl({ + create: [], + read: [], + update: [], + }) + @TableColumn({ + required: true, + isDefaultValueColumn: true, + type: TableColumnType.Boolean, + title: 'Failed Call and SMS Balance Charge Notification Sent to Owners', + description: + 'Failed Call and SMS Balance Charge Notification Sent to Owners', + }) + @Column({ + nullable: false, + default: false, + type: ColumnType.Boolean, + }) + public failedCallAndSMSBalanceChargeNotificationSentToOwners?: boolean = undefined; + + + @ColumnAccessControl({ + create: [], + read: [], + update: [], + }) + @TableColumn({ + required: true, + isDefaultValueColumn: true, + type: TableColumnType.Boolean, + title: 'Failed Call and SMS Balance Charge Notification Sent to Owners', + description: + 'Failed Call and SMS Balance Charge Notification Sent to Owners', + }) + @Column({ + nullable: false, + default: false, + type: ColumnType.Boolean, + }) + public notEnabledSmsNotificationSentToOwners?: boolean = undefined; } diff --git a/Notification/Services/SmsService.ts b/Notification/Services/SmsService.ts index 8abce3f642..16298b12f2 100644 --- a/Notification/Services/SmsService.ts +++ b/Notification/Services/SmsService.ts @@ -61,6 +61,9 @@ export default class SmsService { select: { smsOrCallCurrentBalanceInUSDCents: true, enableSmsNotifications: true, + lowCallAndSMSBalanceNotificationSentToOwners: true, + name: true, + notEnabledSmsNotificationSentToOwners: true }, props: { isRoot: true, @@ -82,12 +85,26 @@ export default class SmsService { 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({ data: smsLog, props: { isRoot: true, }, }); + if (!project.notEnabledSmsNotificationSentToOwners) { + await ProjectService.updateOneById({ + data: { + notEnabledSmsNotificationSentToOwners: + 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; } @@ -107,6 +124,20 @@ export default class SmsService { 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} USD cents. Required balance to send this SMS should is ${SMSDefaultCostInCents} USD cents. Please enable auto recharge or recharge manually.`); + } return; } @@ -122,6 +153,19 @@ export default class SmsService { 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} cents. Required balance is ${SMSDefaultCostInCents} cents to send this SMS. Please enable auto recharge or recharge manually.`); + } return; } } @@ -144,13 +188,14 @@ export default class SmsService { project.smsOrCallCurrentBalanceInUSDCents = Math.floor( project.smsOrCallCurrentBalanceInUSDCents! - - SMSDefaultCostInCents + SMSDefaultCostInCents ); await ProjectService.updateOneById({ data: { smsOrCallCurrentBalanceInUSDCents: project.smsOrCallCurrentBalanceInUSDCents, + notEnabledSmsNotificationSentToOwners: false, // reset this flag }, id: project.id!, props: { diff --git a/Notification/Templates/SimpleMessage.hbs b/Notification/Templates/SimpleMessage.hbs new file mode 100644 index 0000000000..4a747f9e72 --- /dev/null +++ b/Notification/Templates/SimpleMessage.hbs @@ -0,0 +1,11 @@ +{{> Start this}} + +{{> Logo this}} + +{{> EmailTitle title=subject }} + +{{> InfoBlock info=message}} + +{{> Footer this}} + +{{> End}} \ No newline at end of file