From 9e79ca71609fe94a441983ffd8ab5bcd9917dabe Mon Sep 17 00:00:00 2001 From: emmyni <44761757+emmyni@users.noreply.github.com> Date: Mon, 5 Dec 2022 12:10:00 -0500 Subject: [PATCH] [MM-48472]: Update the Upgrade Confirmation Email for yearly subscriptions (#21766) --- api4/cloud.go | 30 ++++++++++++++++++++-- app/app_iface.go | 2 +- app/cloud.go | 4 +-- app/email/email.go | 10 ++++++-- app/email/email_test.go | 38 ++++++++++++++++++++++++++-- app/email/mocks/ServiceInterface.go | 10 ++++---- app/email/service.go | 2 +- app/opentracing/opentracing_layer.go | 4 +-- i18n/en.json | 14 +++++++--- 9 files changed, 94 insertions(+), 20 deletions(-) diff --git a/api4/cloud.go b/api4/cloud.go index 173d975279..3f9106710c 100644 --- a/api4/cloud.go +++ b/api4/cloud.go @@ -136,9 +136,16 @@ func changeSubscription(c *Context, w http.ResponseWriter, r *http.Request) { return } + product, err := c.App.Cloud().GetCloudProduct(c.AppContext.Session().UserId, subscriptionChange.ProductID) + if err != nil || product == nil { + c.Logger.Error("Error finding the new cloud product", mlog.Err(err)) + } + + isYearly := product.IsYearly() + // Log failures for purchase confirmation email, but don't show an error to the user so as not to confuse them // At this point, the upgrade is complete. - if appErr := c.App.SendUpgradeConfirmationEmail(); appErr != nil { + if appErr := c.App.SendUpgradeConfirmationEmail(isYearly); appErr != nil { c.Logger.Error("Error sending purchase confirmation email", mlog.Err(appErr)) } @@ -636,7 +643,26 @@ func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) { return } case model.EventTypeSendUpgradeConfirmationEmail: - if nErr := c.App.SendUpgradeConfirmationEmail(); nErr != nil { + + // isYearly determines whether to send the yearly or monthly Upgrade email + isYearly := false + if event.Subscription != nil && event.CloudWorkspaceOwner != nil { + user, appErr := c.App.GetUserByUsername(event.CloudWorkspaceOwner.UserName) + if appErr != nil { + c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, appErr.Error(), appErr.StatusCode) + return + } + + // Get the current cloud product to determine whether it's a monthly or yearly product + product, err := c.App.Cloud().GetCloudProduct(user.Id, event.Subscription.ProductID) + if err != nil { + c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError) + return + } + isYearly = product.IsYearly() + } + + if nErr := c.App.SendUpgradeConfirmationEmail(isYearly); nErr != nil { c.Err = nErr return } diff --git a/app/app_iface.go b/app/app_iface.go index 948060d0f6..781f12b7e1 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -1038,7 +1038,7 @@ type AppIface interface { SendPasswordReset(email string, siteURL string) (bool, *model.AppError) SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model.AppError SendTestPushNotification(deviceID string) string - SendUpgradeConfirmationEmail() *model.AppError + SendUpgradeConfirmationEmail(isYearly bool) *model.AppError ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, sourcePluginId, destinationPluginId string) SessionHasPermissionTo(session model.Session, permission *model.Permission) bool SessionHasPermissionToAny(session model.Session, permissions []*model.Permission) bool diff --git a/app/cloud.go b/app/cloud.go index fd2d4fb23e..f045c57311 100644 --- a/app/cloud.go +++ b/app/cloud.go @@ -174,7 +174,7 @@ func getNextBillingDateString() string { return fmt.Sprintf("%s %d, %d", t.Month(), t.Day(), t.Year()) } -func (a *App) SendUpgradeConfirmationEmail() *model.AppError { +func (a *App) SendUpgradeConfirmationEmail(isYearly bool) *model.AppError { sysAdmins, e := a.getSysAdminsEmailRecipients() if e != nil { return e @@ -200,7 +200,7 @@ func (a *App) SendUpgradeConfirmationEmail() *model.AppError { name = admin.Username } - err := a.Srv().EmailService.SendCloudUpgradeConfirmationEmail(admin.Email, name, billingDate, admin.Locale, *a.Config().ServiceSettings.SiteURL, subscription.GetWorkSpaceNameFromDNS()) + err := a.Srv().EmailService.SendCloudUpgradeConfirmationEmail(admin.Email, name, billingDate, admin.Locale, *a.Config().ServiceSettings.SiteURL, subscription.GetWorkSpaceNameFromDNS(), isYearly) if err != nil { a.Log().Error("Error sending trial ended email to", mlog.String("email", admin.Email), mlog.Err(err)) countNotOks++ diff --git a/app/email/email.go b/app/email/email.go index b0cd66e7dd..d434d94935 100644 --- a/app/email/email.go +++ b/app/email/email.go @@ -233,13 +233,13 @@ func (es *Service) SendWelcomeEmail(userID string, email string, verified bool, return nil } -func (es *Service) SendCloudUpgradeConfirmationEmail(userEmail, name, date, locale, siteURL, workspaceName string) error { +func (es *Service) SendCloudUpgradeConfirmationEmail(userEmail, name, date, locale, siteURL, workspaceName string, isYearly bool) error { T := i18n.GetUserTranslations(locale) subject := T("api.templates.cloud_upgrade_confirmation.subject") data := es.NewEmailTemplateData(locale) data.Props["Title"] = T("api.templates.cloud_upgrade_confirmation.title") - data.Props["SubTitle"] = T("api.templates.cloud_upgrade_confirmation.subtitle", map[string]any{"WorkspaceName": workspaceName, "Date": date}) + data.Props["SubTitle"] = T("api.templates.cloud_upgrade_confirmation_monthly.subtitle", map[string]any{"WorkspaceName": workspaceName, "Date": date}) data.Props["SiteURL"] = siteURL data.Props["ButtonURL"] = siteURL data.Props["Button"] = T("api.templates.cloud_welcome_email.button") @@ -247,6 +247,12 @@ func (es *Service) SendCloudUpgradeConfirmationEmail(userEmail, name, date, loca data.Props["QuestionInfo"] = T("api.templates.questions_footer.info") data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail + if isYearly { + data.Props["SubTitle"] = T("api.templates.cloud_upgrade_confirmation_yearly.subtitle", map[string]any{"WorkspaceName": workspaceName}) + data.Props["ButtonURL"] = siteURL + "/admin_console/billing/billing_history" + data.Props["Button"] = T("api.templates.cloud_welcome_email.yearly_plan_button") + } + body, err := es.templatesContainer.RenderToString("cloud_upgrade_confirmation", data) if err != nil { return err diff --git a/app/email/email_test.go b/app/email/email_test.go index 0fe3b237e0..2ad00123c3 100644 --- a/app/email/email_test.go +++ b/app/email/email_test.go @@ -258,7 +258,7 @@ func TestSendCloudUpgradedEmail(t *testing.T) { emailTo := "testclouduser@example.com" emailToUsername := strings.Split(emailTo, "@")[0] - t.Run("SendCloudUpgradedEmail", func(t *testing.T) { + t.Run("SendCloudMonthlyUpgradedEmail", func(t *testing.T) { verifyMailbox := func(t *testing.T) { t.Helper() @@ -278,10 +278,44 @@ func TestSendCloudUpgradedEmail(t *testing.T) { require.NoError(t, err, "Could not get message from mailbox") require.Contains(t, resultsEmail.Body.Text, "You are now upgraded!", "Wrong received message %s", resultsEmail.Body.Text) require.Contains(t, resultsEmail.Body.Text, "SomeName workspace has now been upgraded", "Wrong received message %s", resultsEmail.Body.Text) + require.Contains(t, resultsEmail.Body.Text, "You'll be billed from", "Wrong received message %s", resultsEmail.Body.Text) + require.Contains(t, resultsEmail.Body.Text, "Open Mattermost", "Wrong received message %s", resultsEmail.Body.Text) } mail.DeleteMailBox(emailTo) - err := th.service.SendCloudUpgradeConfirmationEmail(emailTo, emailToUsername, "June 23, 2200", th.BasicUser.Locale, "https://example.com", "SomeName") + // Send Update to Monthly Plan email + err := th.service.SendCloudUpgradeConfirmationEmail(emailTo, emailToUsername, "June 23, 2200", th.BasicUser.Locale, "https://example.com", "SomeName", false) + require.NoError(t, err) + + verifyMailbox(t) + }) + + t.Run("SendCloudYearlyUpgradedEmail", func(t *testing.T) { + verifyMailbox := func(t *testing.T) { + t.Helper() + + var resultsMailbox mail.JSONMessageHeaderInbucket + err2 := mail.RetryInbucket(5, func() error { + var err error + resultsMailbox, err = mail.GetMailBox(emailTo) + return err + }) + if err2 != nil { + t.Skipf("No email was received, maybe due load on the server: %v", err2) + } + + require.Len(t, resultsMailbox, 1) + require.Contains(t, resultsMailbox[0].To[0], emailTo, "Wrong To: recipient") + resultsEmail, err := mail.GetMessageFromMailbox(emailTo, resultsMailbox[0].ID) + require.NoError(t, err, "Could not get message from mailbox") + require.Contains(t, resultsEmail.Body.Text, "You are now upgraded!", "Wrong received message %s", resultsEmail.Body.Text) + require.Contains(t, resultsEmail.Body.Text, "SomeName workspace has now been upgraded", "Wrong received message %s", resultsEmail.Body.Text) + require.Contains(t, resultsEmail.Body.Text, "View your invoice", "Wrong received message %s", resultsEmail.Body.Text) + } + mail.DeleteMailBox(emailTo) + + // Send Update to Monthly Plan email + err := th.service.SendCloudUpgradeConfirmationEmail(emailTo, emailToUsername, "June 23, 2200", th.BasicUser.Locale, "https://example.com", "SomeName", true) require.NoError(t, err) verifyMailbox(t) diff --git a/app/email/mocks/ServiceInterface.go b/app/email/mocks/ServiceInterface.go index 08cddbe4ad..98b39f90fe 100644 --- a/app/email/mocks/ServiceInterface.go +++ b/app/email/mocks/ServiceInterface.go @@ -125,13 +125,13 @@ func (_m *ServiceInterface) SendChangeUsernameEmail(newUsername string, _a1 stri return r0 } -// SendCloudUpgradeConfirmationEmail provides a mock function with given fields: userEmail, name, trialEndDate, locale, siteURL, workspaceName -func (_m *ServiceInterface) SendCloudUpgradeConfirmationEmail(userEmail string, name string, trialEndDate string, locale string, siteURL string, workspaceName string) error { - ret := _m.Called(userEmail, name, trialEndDate, locale, siteURL, workspaceName) +// SendCloudUpgradeConfirmationEmail provides a mock function with given fields: userEmail, name, trialEndDate, locale, siteURL, workspaceName, isYearly +func (_m *ServiceInterface) SendCloudUpgradeConfirmationEmail(userEmail string, name string, trialEndDate string, locale string, siteURL string, workspaceName string, isYearly bool) error { + ret := _m.Called(userEmail, name, trialEndDate, locale, siteURL, workspaceName, isYearly) var r0 error - if rf, ok := ret.Get(0).(func(string, string, string, string, string, string) error); ok { - r0 = rf(userEmail, name, trialEndDate, locale, siteURL, workspaceName) + if rf, ok := ret.Get(0).(func(string, string, string, string, string, string, bool) error); ok { + r0 = rf(userEmail, name, trialEndDate, locale, siteURL, workspaceName, isYearly) } else { r0 = ret.Error(0) } diff --git a/app/email/service.go b/app/email/service.go index 0f9158e343..d8745a1735 100644 --- a/app/email/service.go +++ b/app/email/service.go @@ -129,7 +129,7 @@ type ServiceInterface interface { SendVerifyEmail(userEmail, locale, siteURL, token, redirect string) error SendSignInChangeEmail(email, method, locale, siteURL string) error SendWelcomeEmail(userID string, email string, verified bool, disableWelcomeEmail bool, locale, siteURL, redirect string) error - SendCloudUpgradeConfirmationEmail(userEmail, name, trialEndDate, locale, siteURL, workspaceName string) error + SendCloudUpgradeConfirmationEmail(userEmail, name, trialEndDate, locale, siteURL, workspaceName string, isYearly bool) error SendCloudWelcomeEmail(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL string) error SendPasswordChangeEmail(email, method, locale, siteURL string) error SendUserAccessTokenAddedEmail(email, locale, siteURL string) error diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 498451174b..1e25574b70 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -15546,7 +15546,7 @@ func (a *OpenTracingAppLayer) SendTestPushNotification(deviceID string) string { return resultVar0 } -func (a *OpenTracingAppLayer) SendUpgradeConfirmationEmail() *model.AppError { +func (a *OpenTracingAppLayer) SendUpgradeConfirmationEmail(isYearly bool) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendUpgradeConfirmationEmail") @@ -15558,7 +15558,7 @@ func (a *OpenTracingAppLayer) SendUpgradeConfirmationEmail() *model.AppError { }() defer span.Finish() - resultVar0 := a.app.SendUpgradeConfirmationEmail() + resultVar0 := a.app.SendUpgradeConfirmationEmail(isYearly) if resultVar0 != nil { span.LogFields(spanlog.Error(resultVar0)) diff --git a/i18n/en.json b/i18n/en.json index 91c3351aaf..43b65baa82 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3164,12 +3164,16 @@ "translation": "Mattermost Upgrade Confirmation" }, { - "id": "api.templates.cloud_upgrade_confirmation.subtitle", + "id": "api.templates.cloud_upgrade_confirmation.title", + "translation": "You are now upgraded!" + }, + { + "id": "api.templates.cloud_upgrade_confirmation_monthly.subtitle", "translation": "Your {{.WorkspaceName}} workspace has now been upgraded. You'll be billed from {{.Date}}" }, { - "id": "api.templates.cloud_upgrade_confirmation.title", - "translation": "You are now upgraded!" + "id": "api.templates.cloud_upgrade_confirmation_yearly.subtitle", + "translation": "Your {{.WorkspaceName}} workspace has now been upgraded." }, { "id": "api.templates.cloud_welcome_email.add_apps_info", @@ -3239,6 +3243,10 @@ "id": "api.templates.cloud_welcome_email.title", "translation": "Your workspace is ready to go!" }, + { + "id": "api.templates.cloud_welcome_email.yearly_plan_button", + "translation": "View your invoice" + }, { "id": "api.templates.copyright", "translation": "© 2021 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301"