[MM-50534] - Renew email adds portal link when not eligible for self-serve renewal (#22350)

* [MM-50534] - Renew email adds portal link when not eligible for self-serve renewal

* fix translations

* make more checks

* improve endpoint to make checks

* use new checks

* fix translations

* fix lint

* add meaningful names

* rename struct

* rename endpoint
Этот коммит содержится в:
Allan Guwatudde
2023-03-03 18:33:18 +03:00
коммит произвёл GitHub
родитель 9c7aab24f5
Коммит 1f5a6e439e
12 изменённых файлов: 91 добавлений и 69 удалений

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

@@ -41,7 +41,7 @@ func (api *API) InitCloud() {
api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(getSubscription)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription/invoices", api.APISessionRequired(getInvoicesForSubscription)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription/invoices/{invoice_id:[_A-Za-z0-9]+}/pdf", api.APISessionRequired(getSubscriptionInvoicePDF)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription/expand", api.APISessionRequired(GetLicenseExpandStatus)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription/self-serve-status", api.APISessionRequired(getLicenseSelfServeStatus)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(changeSubscription)).Methods("PUT")
// GET /api/v4/cloud/request-trial
@@ -430,7 +430,8 @@ func getCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write(json)
}
func GetLicenseExpandStatus(c *Context, w http.ResponseWriter, r *http.Request) {
// getLicenseSelfServeStatus makes check for the license in the CWS self-serve portal and establishes if the license is renewable, expandable etc.
func getLicenseSelfServeStatus(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
@@ -443,15 +444,15 @@ func GetLicenseExpandStatus(c *Context, w http.ResponseWriter, r *http.Request)
return
}
res, cloudErr := c.App.Cloud().GetLicenseExpandStatus(c.AppContext.Session().UserId, token)
status, cloudErr := c.App.Cloud().GetLicenseSelfServeStatus(c.AppContext.Session().UserId, token)
if cloudErr != nil {
c.Err = model.NewAppError("Api4.GetLicenseExpandStatusForSubscription", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(cloudErr)
c.Err = model.NewAppError("Api4.getLicenseSelfServeStatus", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(cloudErr)
return
}
json, jsonErr := json.Marshal(res)
json, jsonErr := json.Marshal(status)
if jsonErr != nil {
c.Err = model.NewAppError("Api4.GetLicenseExpandStatusForSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
c.Err = model.NewAppError("Api4.getLicenseSelfServeStatus", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
return
}

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

@@ -650,7 +650,7 @@ func TestGetCloudProducts(t *testing.T) {
}
func Test_GetExpandStatsForSubscription(t *testing.T) {
isExpandable := &model.SubscriptionExpandStatus{
status := &model.SubscriptionLicenseSelfServeStatusResponse{
IsExpandable: true,
}
@@ -664,7 +664,7 @@ func Test_GetExpandStatsForSubscription(t *testing.T) {
cloud := mocks.CloudInterface{}
cloud.Mock.On("GetLicenseExpandStatus", mock.Anything).Return(isExpandable, nil)
cloud.Mock.On("GetLicenseSelfServeStatus", mock.Anything).Return(status, nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
@@ -672,9 +672,9 @@ func Test_GetExpandStatsForSubscription(t *testing.T) {
}()
th.App.Srv().Cloud = &cloud
subscriptionExpandable, r, err := th.Client.GetExpandStats(licenseId)
checksMade, r, err := th.Client.GetSubscriptionStatus(licenseId)
require.Error(t, err)
require.Nil(t, subscriptionExpandable)
require.Nil(t, checksMade)
require.Equal(t, http.StatusForbidden, r.StatusCode, "403 Forbidden")
})
@@ -686,7 +686,7 @@ func Test_GetExpandStatsForSubscription(t *testing.T) {
cloud := mocks.CloudInterface{}
cloud.Mock.On("GetLicenseExpandStatus", mock.Anything).Return(isExpandable, nil)
cloud.Mock.On("GetLicenseSelfServeStatus", mock.Anything).Return(status, nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
@@ -694,9 +694,9 @@ func Test_GetExpandStatsForSubscription(t *testing.T) {
}()
th.App.Srv().Cloud = &cloud
subscriptionExpandable, r, err := th.Client.GetExpandStats("")
checks, r, err := th.Client.GetSubscriptionStatus("")
require.Error(t, err)
require.Nil(t, subscriptionExpandable)
require.Nil(t, checks)
require.Equal(t, http.StatusBadRequest, r.StatusCode, "400 Bad Request")
})
}

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

@@ -261,9 +261,14 @@ func requestRenewalLink(c *Context, w http.ResponseWriter, r *http.Request) {
}
// check if it is possible to renew license on the portal with generated token
e := c.App.Cloud().GetLicenseRenewalStatus(c.AppContext.Session().UserId, token)
status, e := c.App.Cloud().GetLicenseSelfServeStatus(c.AppContext.Session().UserId, token)
if e != nil {
c.Err = model.NewAppError("requestRenewalLink", "api.license.request_renewal_link.cannot_renew_on_cws", nil, e.Error(), http.StatusBadRequest)
c.Err = model.NewAppError("requestRenewalLink", "api.license.request_renewal_link.cannot_renew_on_cws", nil, e.Error(), http.StatusInternalServerError)
return
}
if !status.IsRenewable {
c.Err = model.NewAppError("requestRenewalLink", "api.license.request_renewal_link.cannot_renew_on_cws", nil, "License is not self-serve renewable", http.StatusBadRequest)
return
}

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

@@ -989,7 +989,7 @@ func (es *Service) SendLicenseInactivityEmail(email, name, locale, siteURL strin
return nil
}
func (es *Service) SendLicenseUpForRenewalEmail(email, name, locale, siteURL, renewalLink string, daysToExpiration int) error {
func (es *Service) SendLicenseUpForRenewalEmail(email, name, locale, siteURL, ctaTitle, ctaLink, ctaText string, daysToExpiration int) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.license_up_for_renewal_subject")
@@ -997,10 +997,10 @@ func (es *Service) SendLicenseUpForRenewalEmail(email, name, locale, siteURL, re
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.license_up_for_renewal_title")
data.Props["SubTitle"] = T("api.templates.license_up_for_renewal_subtitle", map[string]any{"UserName": name, "Days": daysToExpiration})
data.Props["SubTitleTwo"] = T("api.templates.license_up_for_renewal_subtitle_two")
data.Props["SubTitleTwo"] = ctaTitle
data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
data.Props["Button"] = T("api.templates.license_up_for_renewal_renew_now")
data.Props["ButtonURL"] = renewalLink
data.Props["Button"] = ctaText
data.Props["ButtonURL"] = ctaLink
data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
data.Props["SupportEmail"] = "feedback@mattermost.com"
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
@@ -1298,7 +1298,7 @@ func (es *Service) SendDelinquencyEmail90(email, locale, siteURL string) error {
// 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(renewalLink, email string, locale, siteURL string) error {
func (es *Service) SendRemoveExpiredLicenseEmail(ctaText, ctaLink, email, locale, siteURL string) error {
T := i18n.GetUserTranslations(locale)
subject := T("api.templates.remove_expired_license.subject",
map[string]any{"SiteName": es.config().TeamSettings.SiteName})
@@ -1306,8 +1306,8 @@ func (es *Service) SendRemoveExpiredLicenseEmail(renewalLink, email string, loca
data := es.NewEmailTemplateData(locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = T("api.templates.remove_expired_license.body.title")
data.Props["Link"] = renewalLink
data.Props["LinkButton"] = T("api.templates.remove_expired_license.body.renew_button")
data.Props["Link"] = ctaLink
data.Props["LinkButton"] = ctaText
body, err := es.templatesContainer.RenderToString("remove_expired_license", data)
if err != nil {

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

@@ -358,13 +358,13 @@ func (_m *ServiceInterface) SendLicenseInactivityEmail(_a0 string, name string,
return r0
}
// SendLicenseUpForRenewalEmail provides a mock function with given fields: _a0, name, locale, siteURL, renewalLink, daysToExpiration
func (_m *ServiceInterface) SendLicenseUpForRenewalEmail(_a0 string, name string, locale string, siteURL string, renewalLink string, daysToExpiration int) error {
ret := _m.Called(_a0, name, locale, siteURL, renewalLink, daysToExpiration)
// SendLicenseUpForRenewalEmail provides a mock function with given fields: _a0, name, locale, siteURL, ctaTitle, ctaLink, ctaText, daysToExpiration
func (_m *ServiceInterface) SendLicenseUpForRenewalEmail(_a0 string, name string, locale string, siteURL string, ctaTitle string, ctaLink string, ctaText string, daysToExpiration int) error {
ret := _m.Called(_a0, name, locale, siteURL, ctaTitle, ctaLink, ctaText, daysToExpiration)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string, string, int) error); ok {
r0 = rf(_a0, name, locale, siteURL, renewalLink, daysToExpiration)
if rf, ok := ret.Get(0).(func(string, string, string, string, string, string, string, int) error); ok {
r0 = rf(_a0, name, locale, siteURL, ctaTitle, ctaLink, ctaText, daysToExpiration)
} else {
r0 = ret.Error(0)
}
@@ -484,13 +484,13 @@ func (_m *ServiceInterface) SendPaymentFailedEmail(_a0 string, locale string, fa
return r0, r1
}
// SendRemoveExpiredLicenseEmail provides a mock function with given fields: renewalLink, _a1, locale, siteURL
func (_m *ServiceInterface) SendRemoveExpiredLicenseEmail(renewalLink string, _a1 string, locale string, siteURL string) error {
ret := _m.Called(renewalLink, _a1, locale, siteURL)
// SendRemoveExpiredLicenseEmail provides a mock function with given fields: ctaText, ctaLink, _a2, locale, siteURL
func (_m *ServiceInterface) SendRemoveExpiredLicenseEmail(ctaText string, ctaLink string, _a2 string, locale string, siteURL string) error {
ret := _m.Called(ctaText, ctaLink, _a2, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(renewalLink, _a1, locale, siteURL)
if rf, ok := ret.Get(0).(func(string, string, string, string, string) error); ok {
r0 = rf(ctaText, ctaLink, _a2, locale, siteURL)
} else {
r0 = ret.Error(0)
}

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

@@ -141,7 +141,7 @@ type ServiceInterface interface {
SendDeactivateAccountEmail(email string, locale, siteURL string) error
SendNotificationMail(to, subject, htmlBody string) error
SendMailWithEmbeddedFiles(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, messageID string, inReplyTo string, references string, category string) error
SendLicenseUpForRenewalEmail(email, name, locale, siteURL, renewalLink string, daysToExpiration int) error
SendLicenseUpForRenewalEmail(email, name, locale, siteURL, ctaTitle, ctaLink, ctaText string, daysToExpiration int) error
SendPaymentFailedEmail(email string, locale string, failedPayment *model.FailedPayment, planName, siteURL string) (bool, error)
// Cloud delinquency email sequence
SendDelinquencyEmail7(email, locale, siteURL, planName string) error
@@ -152,7 +152,7 @@ type ServiceInterface interface {
SendDelinquencyEmail75(email, locale, siteURL, planName, delinquencyDate string) error
SendDelinquencyEmail90(email, locale, siteURL string) error
SendNoCardPaymentFailedEmail(email string, locale string, siteURL string) error
SendRemoveExpiredLicenseEmail(renewalLink, email string, locale, siteURL string) error
SendRemoveExpiredLicenseEmail(ctaText, ctaLink, email, locale, siteURL string) error
AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError
GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string
InitEmailBatching()

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

@@ -1288,11 +1288,16 @@ func (s *Server) sendLicenseUpForRenewalEmail(users map[string]*model.User, lice
daysToExpiration := license.DaysToExpiration()
renewalLink, _, appErr := s.GenerateLicenseRenewalLink()
ctaLink, tokenToBeUsedForRenew, appErr := s.GenerateLicenseRenewalLink()
if appErr != nil {
return model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.server.license_up_for_renewal.error_generating_link", nil, "", http.StatusInternalServerError).Wrap(appErr)
}
status, err := s.Cloud.GetLicenseSelfServeStatus("", tokenToBeUsedForRenew)
if err != nil {
return model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// we want to at least have one email sent out to an admin
countNotOks := 0
@@ -1301,7 +1306,16 @@ func (s *Server) sendLicenseUpForRenewalEmail(users map[string]*model.User, lice
if name == "" {
name = user.Username
}
if err := s.EmailService.SendLicenseUpForRenewalEmail(user.Email, name, user.Locale, *s.platform.Config().ServiceSettings.SiteURL, renewalLink, daysToExpiration); err != nil {
T := i18n.GetUserTranslations(user.Locale)
ctaTitle := T("api.templates.license_up_for_renewal_subtitle_two")
ctaText := T("api.templates.license_up_for_renewal_renew_now")
if !status.IsRenewable {
ctaTitle = ""
ctaText = T("api.templates.license_up_for_renewal_contact_sales")
ctaLink = "https://mattermost.com/contact-sales/"
}
if err := s.EmailService.SendLicenseUpForRenewalEmail(user.Email, name, user.Locale, *s.platform.Config().ServiceSettings.SiteURL, ctaTitle, ctaLink, ctaText, daysToExpiration); err != nil {
mlog.Error("Error sending license up for renewal email to", mlog.String("user_email", user.Email), mlog.Err(err))
countNotOks++
}
@@ -1366,9 +1380,15 @@ func (s *Server) doLicenseExpirationCheck() {
return
}
renewalLink, _, appErr := s.GenerateLicenseRenewalLink()
ctaLink, tokenToBeUsedForRenew, appErr := s.GenerateLicenseRenewalLink()
if appErr != nil {
mlog.Error("Error while sending the license expired email.", mlog.Err(appErr))
mlog.Debug(model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.server.license_up_for_renewal.error_generating_link", nil, "", http.StatusInternalServerError).Wrap(appErr).Error())
return
}
status, err := s.Cloud.GetLicenseSelfServeStatus("", tokenToBeUsedForRenew)
if err != nil {
mlog.Debug(model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err).Error())
return
}
@@ -1380,9 +1400,16 @@ func (s *Server) doLicenseExpirationCheck() {
continue
}
T := i18n.GetUserTranslations(user.Locale)
ctaText := T("api.templates.remove_expired_license.body.renew_button")
if !status.IsRenewable {
ctaText = T("api.templates.license_up_for_renewal_contact_sales")
ctaLink = "https://mattermost.com/contact-sales/"
}
mlog.Debug("Sending license expired email.", mlog.String("user_email", user.Email))
s.Go(func() {
if err := s.SendRemoveExpiredLicenseEmail(user.Email, renewalLink, user.Locale, *s.platform.Config().ServiceSettings.SiteURL); err != nil {
if err := s.SendRemoveExpiredLicenseEmail(user.Email, ctaText, ctaLink, user.Locale, *s.platform.Config().ServiceSettings.SiteURL); err != nil {
mlog.Error("Error while sending the license expired email.", mlog.String("user_email", user.Email), mlog.Err(err))
}
})
@@ -1394,9 +1421,9 @@ func (s *Server) doLicenseExpirationCheck() {
// 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 (s *Server) SendRemoveExpiredLicenseEmail(email string, renewalLink, locale, siteURL string) *model.AppError {
func (s *Server) SendRemoveExpiredLicenseEmail(email, ctaText, ctaLink, locale, siteURL string) *model.AppError {
if err := s.EmailService.SendRemoveExpiredLicenseEmail(renewalLink, email, locale, siteURL); err != nil {
if err := s.EmailService.SendRemoveExpiredLicenseEmail(ctaText, ctaLink, email, locale, siteURL); err != nil {
return model.NewAppError("SendRemoveExpiredLicenseEmail", "api.license.remove_expired_license.failed.error", nil, "", http.StatusInternalServerError).Wrap(err)
}

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

@@ -17,7 +17,7 @@ type CloudInterface interface {
ConfirmCustomerPayment(userID string, confirmRequest *model.ConfirmPaymentMethodRequest) error
GetCloudCustomer(userID string) (*model.CloudCustomer, error)
GetLicenseExpandStatus(userID string, token string) (*model.SubscriptionExpandStatus, error)
GetLicenseSelfServeStatus(userID string, token string) (*model.SubscriptionLicenseSelfServeStatusResponse, error)
UpdateCloudCustomer(userID string, customerInfo *model.CloudCustomerInfo) (*model.CloudCustomer, error)
UpdateCloudCustomerAddress(userID string, address *model.Address) (*model.CloudCustomer, error)
@@ -30,8 +30,6 @@ type CloudInterface interface {
RequestCloudTrial(userID, subscriptionID, newValidBusinessEmail string) (*model.Subscription, error)
ValidateBusinessEmail(userID, email string) error
// GetLicenseRenewalStatus checks on the portal whether it is possible to use token to renew a license
GetLicenseRenewalStatus(userID, token string) error
InvalidateCaches() error
// hosted customer methods

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

@@ -339,16 +339,16 @@ func (_m *CloudInterface) GetInvoicesForSubscription(userID string) ([]*model.In
return r0, r1
}
// GetLicenseExpandStatus provides a mock function with given fields: userID, token
func (_m *CloudInterface) GetLicenseExpandStatus(userID string, token string) (*model.SubscriptionExpandStatus, error) {
// GetLicenseSelfServeStatus provides a mock function with given fields: userID, token
func (_m *CloudInterface) GetLicenseSelfServeStatus(userID string, token string) (*model.SubscriptionLicenseSelfServeStatusResponse, error) {
ret := _m.Called(userID, token)
var r0 *model.SubscriptionExpandStatus
if rf, ok := ret.Get(0).(func(string, string) *model.SubscriptionExpandStatus); ok {
var r0 *model.SubscriptionLicenseSelfServeStatusResponse
if rf, ok := ret.Get(0).(func(string, string) *model.SubscriptionLicenseSelfServeStatusResponse); ok {
r0 = rf(userID, token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.SubscriptionExpandStatus)
r0 = ret.Get(0).(*model.SubscriptionLicenseSelfServeStatusResponse)
}
}
@@ -362,20 +362,6 @@ func (_m *CloudInterface) GetLicenseExpandStatus(userID string, token string) (*
return r0, r1
}
// GetLicenseRenewalStatus provides a mock function with given fields: userID, token
func (_m *CloudInterface) GetLicenseRenewalStatus(userID string, token string) error {
ret := _m.Called(userID, token)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(userID, token)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetSelfHostedInvoicePDF provides a mock function with given fields: invoiceID
func (_m *CloudInterface) GetSelfHostedInvoicePDF(invoiceID string) ([]byte, string, error) {
ret := _m.Called(invoiceID)

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

@@ -3623,6 +3623,10 @@
"id": "api.templates.invite_team_and_channels_subject",
"translation": "[{{ .SiteName }}] {{ .SenderName }} invited you to join {{ .ChannelsLen }} channels on the {{ .TeamDisplayName }} Team"
},
{
"id": "api.templates.license_up_for_renewal_contact_sales",
"translation": "Contact sales"
},
{
"id": "api.templates.license_up_for_renewal_renew_now",
"translation": "Renew now"

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

@@ -8235,17 +8235,17 @@ func (c *Client4) GetCloudCustomer() (*CloudCustomer, *Response, error) {
return cloudCustomer, BuildResponse(r), nil
}
func (c *Client4) GetExpandStats(licenseId string) (*SubscriptionExpandStatus, *Response, error) {
r, err := c.DoAPIGet(fmt.Sprintf("%s%s?licenseID=%s", c.cloudRoute(), "/subscription/expand", licenseId), "")
func (c *Client4) GetSubscriptionStatus(licenseId string) (*SubscriptionLicenseSelfServeStatusResponse, *Response, error) {
r, err := c.DoAPIGet(fmt.Sprintf("%s%s?licenseID=%s", c.cloudRoute(), "/subscription/self-serve-status", licenseId), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var subscriptionExpandable *SubscriptionExpandStatus
json.NewDecoder(r.Body).Decode(&subscriptionExpandable)
var status *SubscriptionLicenseSelfServeStatusResponse
json.NewDecoder(r.Body).Decode(&status)
return subscriptionExpandable, BuildResponse(r), nil
return status, BuildResponse(r), nil
}
func (c *Client4) GetSubscription() (*Subscription, *Response, error) {

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

@@ -125,8 +125,9 @@ type ValidateBusinessEmailResponse struct {
IsValid bool `json:"is_valid"`
}
type SubscriptionExpandStatus struct {
type SubscriptionLicenseSelfServeStatusResponse struct {
IsExpandable bool `json:"is_expandable"`
IsRenewable bool `json:"is_renewable"`
}
// CloudCustomerInfo represents editable info of a customer.