[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 <build@mattermost.com>
Co-authored-by: Gabe Jackson <3694686+gabrieljackson@users.noreply.github.com>
Этот коммит содержится в:
Nick Misasi
2024-01-11 13:07:41 -05:00
коммит произвёл GitHub
родитель 04cf1ed114
Коммит aafe7439af
15 изменённых файлов: 889 добавлений и 1 удалений

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -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))
}
}

Просмотреть файл

@@ -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 {

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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")

Просмотреть файл

@@ -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
}