From aafe7439afb96b3f014da2a10aeb4db0583f0c37 Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Thu, 11 Jan 2024 13:07:41 -0500 Subject: [PATCH] [CLD-6894] Add 60, 30, and 7 day reminder emails for Cloud Renewals (#25883) * Add email notifications for Cloud Renewals * Updates * Updates * Update app-layers * make build-templates * Add ability to set an env variable as a unix timestamp in s as the current date when getting DaysToExpiration * Add a mechanism to ensure at least one admin receives every email --------- Co-authored-by: Mattermost Build Co-authored-by: Gabe Jackson <3694686+gabrieljackson@users.noreply.github.com> --- server/channels/app/app_iface.go | 1 + server/channels/app/cloud.go | 97 ++++ server/channels/app/email/email.go | 93 +++ .../app/email/mocks/ServiceInterface.go | 42 ++ server/channels/app/email/service.go | 3 + .../app/opentracing/opentracing_layer.go | 15 + server/channels/app/server.go | 3 +- server/i18n/en.json | 32 ++ server/public/model/cloud.go | 18 + server/public/model/system.go | 1 + .../templates/cloud_renewal_notification.html | 530 ++++++++++++++++++ .../templates/cloud_renewal_notification.mjml | 53 ++ .../src/images/payment_processing.png | Bin 0 -> 16536 bytes webapp/channels/src/images/purchase_alert.png | Bin 0 -> 8163 bytes webapp/channels/webpack.config.js | 2 + 15 files changed, 889 insertions(+), 1 deletion(-) create mode 100644 server/templates/cloud_renewal_notification.html create mode 100644 server/templates/cloud_renewal_notification.mjml create mode 100644 webapp/channels/src/images/payment_processing.png create mode 100644 webapp/channels/src/images/purchase_alert.png diff --git a/server/channels/app/app_iface.go b/server/channels/app/app_iface.go index 050d2cf9d6..d58b1a5b96 100644 --- a/server/channels/app/app_iface.go +++ b/server/channels/app/app_iface.go @@ -573,6 +573,7 @@ type AppIface interface { DoLocalRequest(c request.CTX, rawURL string, body []byte) (*http.Response, *model.AppError) DoLogin(c request.CTX, w http.ResponseWriter, r *http.Request, user *model.User, deviceID string, isMobile, isOAuthUser, isSaml bool) (*model.Session, *model.AppError) DoPostActionWithCookie(c request.CTX, postID, actionId, userID, selectedOption string, cookie *model.PostActionCookie) (string, *model.AppError) + DoSubscriptionRenewalCheck() DoSystemConsoleRolesCreationMigration() DoUploadFile(c request.CTX, now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) DoUploadFileExpectModification(c request.CTX, now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, []byte, *model.AppError) diff --git a/server/channels/app/cloud.go b/server/channels/app/cloud.go index 57d7e79956..2f8909b8f0 100644 --- a/server/channels/app/cloud.go +++ b/server/channels/app/cloud.go @@ -8,11 +8,13 @@ import ( "fmt" "io" "net/http" + "strconv" "time" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/v8/channels/product" + "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/einterfaces" ) @@ -266,3 +268,98 @@ func (a *App) SendSubscriptionHistoryEvent(userID string) (*model.SubscriptionHi } return a.Cloud().CreateOrUpdateSubscriptionHistoryEvent(userID, int(userCount)) } + +func (a *App) DoSubscriptionRenewalCheck() { + if !a.License().IsCloud() || !a.Config().FeatureFlags.CloudAnnualRenewals { + return + } + + subscription, err := a.Cloud().GetSubscription("") + if err != nil { + a.Log().Error("Error getting subscription", mlog.Err(err)) + return + } + + if subscription == nil { + a.Log().Error("Subscription not found") + return + } + + sysVar, err := a.Srv().Store().System().GetByName(model.CloudRenewalEmail) + if err != nil { + // We only care about the error if it wasn't a not found error + if _, ok := err.(*store.ErrNotFound); !ok { + a.Log().Error(err.Error()) + } + } + + prevSentEmail := int64(0) + if sysVar != nil { + // We don't care about parse errors because it's possible the value is empty, and we've already defaulted to 0 + prevSentEmail, _ = strconv.ParseInt(sysVar.Value, 10, 64) + } + + if subscription.WillRenew == "true" { + // They've already completed the renewal process so no need to email them. + // We can zero out the system variable so that this process will work again next year + if prevSentEmail != 0 { + sysVar.Value = "0" + err = a.Srv().Store().System().SaveOrUpdate(sysVar) + if err != nil { + a.Log().Error("Error saving system variable", mlog.Err(err)) + } + } + return + } + + var emailFunc func(email, locale, siteURL string) error + + daysToExpiration := subscription.DaysToExpiration() + + // Only send the email if within the period and it's not already been sent + // This allows the email to send on day 59 if for whatever reason it was unable to on day 60 + if daysToExpiration <= 60 && daysToExpiration > 30 && prevSentEmail != 60 { + emailFunc = a.Srv().EmailService.SendCloudRenewalEmail60 + prevSentEmail = 60 + } else if daysToExpiration <= 30 && daysToExpiration > 7 && prevSentEmail != 30 { + emailFunc = a.Srv().EmailService.SendCloudRenewalEmail30 + prevSentEmail = 30 + } else if daysToExpiration <= 7 && daysToExpiration > 3 && prevSentEmail != 7 { + emailFunc = a.Srv().EmailService.SendCloudRenewalEmail7 + prevSentEmail = 7 + } + + if emailFunc == nil { + return + } + + sysAdmins, aErr := a.getSysAdminsEmailRecipients() + if aErr != nil { + a.Log().Error("Error getting sys admins", mlog.Err(aErr)) + return + } + + numFailed := 0 + for _, admin := range sysAdmins { + err = emailFunc(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL) + if err != nil { + a.Log().Error("Error sending renewal email", mlog.Err(err)) + numFailed += 1 + } + } + + if numFailed == len(sysAdmins) { + // If all emails failed, we don't want to update the system variable + return + } + + updatedSysVar := &model.System{ + Name: model.CloudRenewalEmail, + Value: strconv.FormatInt(prevSentEmail, 10), + } + + err = a.Srv().Store().System().SaveOrUpdate(updatedSysVar) + if err != nil { + a.Log().Error("Error saving system variable", mlog.Err(err)) + } +} diff --git a/server/channels/app/email/email.go b/server/channels/app/email/email.go index 3ef23f74ab..c48620be1f 100644 --- a/server/channels/app/email/email.go +++ b/server/channels/app/email/email.go @@ -1251,6 +1251,99 @@ func (es *Service) SendDelinquencyEmail90(email, locale, siteURL string) error { return nil } +func (es *Service) SendCloudRenewalEmail60(email, locale, siteURL string) error { + T := i18n.GetUserTranslations(locale) + + subject := T("api.templates.cloud_renewal_60.subject") + + data := es.NewEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.cloud_renewal_60.title") + data.Props["SubTitle"] = T("api.templates.cloud_renewal.subtitle") + // TODO: use the open delinquency modal action + data.Props["ButtonURL"] = siteURL + "/admin_console/billing/subscription" + data.Props["Button"] = T("api.templates.cloud_renewal.button") + + data.Props["QuestionTitle"] = T("api.templates.questions_footer.title") + data.Props["QuestionInfo"] = T("api.templates.questions_footer.info") + data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") + data.Props["Image"] = "payment_processing.png" + + body, err := es.templatesContainer.RenderToString("cloud_renewal_notification", data) + if err != nil { + return err + } + + if err := es.sendMail(email, subject, body, "CloudRenewal60"); err != nil { + return err + } + + return nil +} + +func (es *Service) SendCloudRenewalEmail30(email, locale, siteURL string) error { + T := i18n.GetUserTranslations(locale) + + subject := T("api.templates.cloud_renewal_30.subject") + + data := es.NewEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.cloud_renewal_30.title") + data.Props["SubTitle"] = T("api.templates.cloud_renewal.subtitle") + // TODO: use the open delinquency modal action + data.Props["ButtonURL"] = siteURL + "/admin_console/billing/subscription" + data.Props["Button"] = T("api.templates.cloud_renewal.button") + + data.Props["QuestionTitle"] = T("api.templates.questions_footer.title") + data.Props["QuestionInfo"] = T("api.templates.questions_footer.info") + data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") + data.Props["Image"] = "payment_processing.png" + + body, err := es.templatesContainer.RenderToString("cloud_renewal_notification", data) + if err != nil { + return err + } + + if err := es.sendMail(email, subject, body, "CloudRenewal30"); err != nil { + return err + } + + return nil +} + +func (es *Service) SendCloudRenewalEmail7(email, locale, siteURL string) error { + T := i18n.GetUserTranslations(locale) + + subject := T("api.templates.cloud_renewal_7.subject") + + data := es.NewEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.cloud_renewal_7.title") + data.Props["SubTitle"] = T("api.templates.cloud_renewal.subtitle") + // TODO: use the open delinquency modal action + data.Props["ButtonURL"] = siteURL + "/admin_console/billing/subscription" + data.Props["Button"] = T("api.templates.cloud_renewal.button") + + data.Props["QuestionTitle"] = T("api.templates.questions_footer.title") + data.Props["QuestionInfo"] = T("api.templates.questions_footer.info") + data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") + data.Props["Image"] = "purchase_alert.png" + + body, err := es.templatesContainer.RenderToString("cloud_renewal_notification", data) + if err != nil { + return err + } + + if err := es.sendMail(email, subject, body, "CloudRenewal7"); err != nil { + return err + } + + return nil +} + // SendRemoveExpiredLicenseEmail formats an email and uses the email service to send the email to user with link pointing to CWS // to renew the user license func (es *Service) SendRemoveExpiredLicenseEmail(ctaText, ctaLink, email, locale, siteURL string) error { diff --git a/server/channels/app/email/mocks/ServiceInterface.go b/server/channels/app/email/mocks/ServiceInterface.go index 8597124ea8..deaca07976 100644 --- a/server/channels/app/email/mocks/ServiceInterface.go +++ b/server/channels/app/email/mocks/ServiceInterface.go @@ -128,6 +128,48 @@ func (_m *ServiceInterface) SendChangeUsernameEmail(newUsername string, _a1 stri return r0 } +// SendCloudRenewalEmail30 provides a mock function with given fields: _a0, locale, siteURL +func (_m *ServiceInterface) SendCloudRenewalEmail30(_a0 string, locale string, siteURL string) error { + ret := _m.Called(_a0, locale, siteURL) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, string) error); ok { + r0 = rf(_a0, locale, siteURL) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendCloudRenewalEmail60 provides a mock function with given fields: _a0, locale, siteURL +func (_m *ServiceInterface) SendCloudRenewalEmail60(_a0 string, locale string, siteURL string) error { + ret := _m.Called(_a0, locale, siteURL) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, string) error); ok { + r0 = rf(_a0, locale, siteURL) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendCloudRenewalEmail7 provides a mock function with given fields: _a0, locale, siteURL +func (_m *ServiceInterface) SendCloudRenewalEmail7(_a0 string, locale string, siteURL string) error { + ret := _m.Called(_a0, locale, siteURL) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, string) error); ok { + r0 = rf(_a0, locale, siteURL) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // SendCloudUpgradeConfirmationEmail provides a mock function with given fields: userEmail, name, trialEndDate, locale, siteURL, workspaceName, isYearly, embeddedFiles func (_m *ServiceInterface) SendCloudUpgradeConfirmationEmail(userEmail string, name string, trialEndDate string, locale string, siteURL string, workspaceName string, isYearly bool, embeddedFiles map[string]io.Reader) error { ret := _m.Called(userEmail, name, trialEndDate, locale, siteURL, workspaceName, isYearly, embeddedFiles) diff --git a/server/channels/app/email/service.go b/server/channels/app/email/service.go index af57bc0e7c..adf21e531e 100644 --- a/server/channels/app/email/service.go +++ b/server/channels/app/email/service.go @@ -156,6 +156,9 @@ type ServiceInterface interface { SendDelinquencyEmail60(email, locale, siteURL string) error SendDelinquencyEmail75(email, locale, siteURL, planName, delinquencyDate string) error SendDelinquencyEmail90(email, locale, siteURL string) error + SendCloudRenewalEmail60(email, locale, siteURL string) error + SendCloudRenewalEmail30(email, locale, siteURL string) error + SendCloudRenewalEmail7(email, locale, siteURL string) error SendNoCardPaymentFailedEmail(email string, locale string, siteURL string) error SendRemoveExpiredLicenseEmail(ctaText, ctaLink, email, locale, siteURL string) error AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError diff --git a/server/channels/app/opentracing/opentracing_layer.go b/server/channels/app/opentracing/opentracing_layer.go index b7809de9c7..3fd2777aba 100644 --- a/server/channels/app/opentracing/opentracing_layer.go +++ b/server/channels/app/opentracing/opentracing_layer.go @@ -3894,6 +3894,21 @@ func (a *OpenTracingAppLayer) DoPostActionWithCookie(c request.CTX, postID strin return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) DoSubscriptionRenewalCheck() { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoSubscriptionRenewalCheck") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + a.app.DoSubscriptionRenewalCheck() +} + func (a *OpenTracingAppLayer) DoSystemConsoleRolesCreationMigration() { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoSystemConsoleRolesCreationMigration") diff --git a/server/channels/app/server.go b/server/channels/app/server.go index 48e2d56ef8..1394b783cc 100644 --- a/server/channels/app/server.go +++ b/server/channels/app/server.go @@ -1415,7 +1415,8 @@ func (s *Server) doLicenseExpirationCheck() { } if license.IsCloud() { - mlog.Debug("Skipping license expiration check for Cloud") + appInstance := New(ServerConnector(s.Channels())) + appInstance.DoSubscriptionRenewalCheck() return } diff --git a/server/i18n/en.json b/server/i18n/en.json index 8804850223..bba5af5b12 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -3378,6 +3378,38 @@ "id": "api.team.update_team_scheme.scheme_scope.error", "translation": "Unable to set the scheme to the team because the supplied scheme is not a team scheme." }, + { + "id": "api.templates.cloud_renewal.button", + "translation": "Renew now" + }, + { + "id": "api.templates.cloud_renewal.subtitle", + "translation": "Please renew to avoid any disruption" + }, + { + "id": "api.templates.cloud_renewal_30.subject", + "translation": "Annual bill due in 30 days" + }, + { + "id": "api.templates.cloud_renewal_30.title", + "translation": "Your annual bill is due in 30 days" + }, + { + "id": "api.templates.cloud_renewal_60.subject", + "translation": "Annual subscription renewal in 60 days" + }, + { + "id": "api.templates.cloud_renewal_60.title", + "translation": "Annual subscription renewal in 60 days" + }, + { + "id": "api.templates.cloud_renewal_7.subject", + "translation": "Action Required: Annual subscription renewal in 7 days" + }, + { + "id": "api.templates.cloud_renewal_7.title", + "translation": "You are about to lose access to your workspace in 7 days" + }, { "id": "api.templates.cloud_upgrade_confirmation.subject", "translation": "Mattermost Upgrade Confirmation" diff --git a/server/public/model/cloud.go b/server/public/model/cloud.go index 8c367240be..3dba37d279 100644 --- a/server/public/model/cloud.go +++ b/server/public/model/cloud.go @@ -5,7 +5,10 @@ package model import ( "encoding/json" + "os" + "strconv" "strings" + "time" ) const ( @@ -184,6 +187,21 @@ type Subscription struct { WillRenew string `json:"will_renew"` } +func (s *Subscription) DaysToExpiration() int64 { + now := time.Now().UnixMilli() + // Allows us to base the current time off of an environment variable for testing purposes + if GetServiceEnvironment() == ServiceEnvironmentTest { + if currTime, set := os.LookupEnv("CLOUD_MOCK_CURRENT_TIME"); set { + timeInt, err := strconv.ParseInt(currTime, 10, 64) + if err == nil { + now = time.Unix(timeInt, 0).UnixMilli() + } + } + } + daysToExpiry := (s.EndAt - now) / (1000 * 60 * 60 * 24) + return daysToExpiry +} + // Subscription History model represents true up event in a yearly subscription type SubscriptionHistory struct { ID string `json:"id"` diff --git a/server/public/model/system.go b/server/public/model/system.go index 3a292fe672..f830f05388 100644 --- a/server/public/model/system.go +++ b/server/public/model/system.go @@ -38,6 +38,7 @@ const ( SystemHostedPurchaseNeedsScreening = "HostedPurchaseNeedsScreening" AwsMeteringReportInterval = 1 AwsMeteringDimensionUsageHrs = "UsageHrs" + CloudRenewalEmail = "CloudRenewalEmail" ) const ( diff --git a/server/templates/cloud_renewal_notification.html b/server/templates/cloud_renewal_notification.html new file mode 100644 index 0000000000..0977a8844b --- /dev/null +++ b/server/templates/cloud_renewal_notification.html @@ -0,0 +1,530 @@ +{{define "cloud_renewal_notification"}} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + +
+
{{.Props.Title}}
+
+
{{.Props.SubTitle}}
+
+ + + + +
+ + {{.Props.Button}} + +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + +
+
{{.Props.QuestionTitle}}
+
+
{{.Props.QuestionInfo}} + + {{.Props.SupportEmail}} +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+
{{.Props.Organization}} + {{.Props.FooterV2}} +
+
+
+ +
+
+ +
+
+ +
+ + + + +{{end}} diff --git a/server/templates/cloud_renewal_notification.mjml b/server/templates/cloud_renewal_notification.mjml new file mode 100644 index 0000000000..a81a3de661 --- /dev/null +++ b/server/templates/cloud_renewal_notification.mjml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + {{.Props.Title}} + + + {{.Props.SubTitle}} + + {{.Props.Button}} + + + + + + + + + + + {{.Props.QuestionTitle}} + + + {{.Props.QuestionInfo}} + + {{.Props.SupportEmail}} + + + + + + + {{.Props.Organization}} + {{.Props.FooterV2}} + + + + + + + \ No newline at end of file diff --git a/webapp/channels/src/images/payment_processing.png b/webapp/channels/src/images/payment_processing.png new file mode 100644 index 0000000000000000000000000000000000000000..b83638708331135165d942df1cee33c5c49790f1 GIT binary patch literal 16536 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 F{|_>KLgN4c literal 0 HcmV?d00001 diff --git a/webapp/channels/src/images/purchase_alert.png b/webapp/channels/src/images/purchase_alert.png new file mode 100644 index 0000000000000000000000000000000000000000..9e9ca273e9411579c3aa363f42ae6449f56fd4f0 GIT binary patch literal 8163 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 literal 0 HcmV?d00001 diff --git a/webapp/channels/webpack.config.js b/webapp/channels/webpack.config.js index 7e0d8992ce..67897c5169 100644 --- a/webapp/channels/webpack.config.js +++ b/webapp/channels/webpack.config.js @@ -191,6 +191,8 @@ var config = { {from: 'src/images/cloud-laptop-error.png', to: 'images'}, {from: 'src/images/cloud-laptop-warning.png', to: 'images'}, {from: 'src/images/cloud-upgrade-person-hand-to-face.png', to: 'images'}, + {from: 'src/images/payment_processing.png', to: 'images'}, + {from: 'src/images/purchase_alert.png', to: 'images'}, {from: '../node_modules/pdfjs-dist/cmaps', to: 'cmaps'}, ], }),