[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>
Этот коммит содержится в:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user