diff --git a/api4/cloud.go b/api4/cloud.go index 655eddd4ad..9e10aafc08 100644 --- a/api4/cloud.go +++ b/api4/cloud.go @@ -665,6 +665,18 @@ func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) { return } c.Logger.Info("Updated subscription from webhook event") + case model.EventTypeTriggerDelinquencyEmail: + var emailToTrigger model.DelinquencyEmail + if event.DelinquencyEmail != nil { + emailToTrigger = model.DelinquencyEmail(event.DelinquencyEmail.EmailToTrigger) + } else { + c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.delinquency_email.missing_email_to_trigger", nil, "", http.StatusInternalServerError) + return + } + if nErr := c.App.SendDelinquencyEmail(emailToTrigger); nErr != nil { + c.Err = nErr + return + } default: c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.cws_webhook_event_missing_error", nil, "", http.StatusNotFound) diff --git a/app/app_iface.go b/app/app_iface.go index 561ec1b7c3..fa2a68696b 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -1017,6 +1017,7 @@ type AppIface interface { SendAckToPushProxy(ack *model.PushNotificationAck) error SendAutoResponse(c request.CTX, channel *model.Channel, receiver *model.User, post *model.Post) (bool, *model.AppError) SendAutoResponseIfNecessary(c request.CTX, channel *model.Channel, sender *model.User, post *model.Post) (bool, *model.AppError) + SendDelinquencyEmail(emailToSend model.DelinquencyEmail) *model.AppError SendEmailVerification(user *model.User, newEmail, redirect string) *model.AppError SendEphemeralPost(c request.CTX, userID string, post *model.Post) *model.Post SendNotifications(c request.CTX, post *model.Post, team *model.Team, channel *model.Channel, sender *model.User, parentPostList *model.PostList, setOnline bool) ([]string, error) diff --git a/app/cloud.go b/app/cloud.go index bb38e6bb22..fed00db27b 100644 --- a/app/cloud.go +++ b/app/cloud.go @@ -148,6 +148,87 @@ func (a *App) SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model. return nil } +func getCurrentProduct(subscriptionProductID string, products []*model.Product) *model.Product { + for _, product := range products { + if product.ID == subscriptionProductID { + return product + } + } + return nil +} + +func (a *App) SendDelinquencyEmail(emailToSend model.DelinquencyEmail) *model.AppError { + sysAdmins, aErr := a.getSysAdminsEmailRecipients() + if aErr != nil { + return aErr + } + subscription, err := a.Cloud().GetSubscription("") + if err != nil { + return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_subscription.app_error", nil, err.Error(), http.StatusInternalServerError) + } + if subscription == nil { + return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_subscription.app_error", nil, "", http.StatusInternalServerError) + } + + products, err := a.Cloud().GetCloudProducts("", false) + if err != nil { + return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_cloud_products.app_error", nil, err.Error(), http.StatusInternalServerError) + } + if products == nil { + return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_cloud_products.app_error", nil, "", http.StatusInternalServerError) + } + + planName := getCurrentProduct(subscription.ProductID, products).Name + + if subscription.DelinquentSince == nil { + return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_subscription_delinquency_date.app_error", nil, "", http.StatusInternalServerError) + } + + delinquentSince := time.Unix(*subscription.DelinquentSince, 0) + + delinquencyDate := delinquentSince.Format("01/02/2006") + for _, admin := range sysAdmins { + switch emailToSend { + case model.DelinquencyEmail7: + err := a.Srv().EmailService.SendDelinquencyEmail7(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName) + if err != nil { + a.Log().Error("Error sending delinquency email 7", mlog.Err(err)) + } + case model.DelinquencyEmail14: + err := a.Srv().EmailService.SendDelinquencyEmail14(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName) + if err != nil { + a.Log().Error("Error sending delinquency email 14", mlog.Err(err)) + } + case model.DelinquencyEmail30: + err := a.Srv().EmailService.SendDelinquencyEmail30(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName) + if err != nil { + a.Log().Error("Error sending delinquency email 30", mlog.Err(err)) + } + case model.DelinquencyEmail45: + err := a.Srv().EmailService.SendDelinquencyEmail45(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName, delinquencyDate) + if err != nil { + a.Log().Error("Error sending delinquency email 45", mlog.Err(err)) + } + case model.DelinquencyEmail60: + err := a.Srv().EmailService.SendDelinquencyEmail60(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL) + if err != nil { + a.Log().Error("Error sending delinquency email 60", mlog.Err(err)) + } + case model.DelinquencyEmail75: + err := a.Srv().EmailService.SendDelinquencyEmail75(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName, delinquencyDate) + if err != nil { + a.Log().Error("Error sending delinquency email 75", mlog.Err(err)) + } + case model.DelinquencyEmail90: + err := a.Srv().EmailService.SendDelinquencyEmail90(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL) + if err != nil { + a.Log().Error("Error sending delinquency email 90", mlog.Err(err)) + } + } + } + return nil +} + func (a *App) AdjustInProductLimits(limits *model.ProductLimits, subscription *model.Subscription) *model.AppError { if limits.Teams != nil && limits.Teams.Active != nil && *limits.Teams.Active > 0 { err := a.AdjustTeamsFromProductLimits(limits.Teams) diff --git a/app/email/email.go b/app/email/email.go index 10c3ce0d9f..e3928d9b9b 100644 --- a/app/email/email.go +++ b/app/email/email.go @@ -1000,6 +1000,225 @@ func (es *Service) SendNoCardPaymentFailedEmail(email string, locale string, sit return nil } +func (es *Service) SendDelinquencyEmail7(email, locale, siteURL, planName string) error { + T := i18n.GetUserTranslations(locale) + + subject := T("api.templates.payment_failed.subject") + + data := es.NewEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.delinquency_7.title") + data.Props["SubTitle1"] = T("api.templates.delinquency_7.subtitle1") + data.Props["SubTitle2"] = T("api.templates.delinquency_7.subtitle2", map[string]any{"Plan": planName}) + 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["Button"] = T("api.templates.delinquency_7.button") + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") + + data.Props["Footer"] = T("api.templates.copyright") + + body, err := es.templatesContainer.RenderToString("cloud_7_day_arrears", data) + if err != nil { + return err + } + + if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail); err != nil { + return err + } + + return nil +} +func (es *Service) SendDelinquencyEmail14(email, locale, siteURL, planName string) error { + T := i18n.GetUserTranslations(locale) + + subject := T("api.templates.delinquency_14.subject", map[string]any{"Plan": planName}) + + data := es.NewEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.delinquency_14.title") + data.Props["SubTitle1"] = T("api.templates.delinquency_14.subtitle1") + data.Props["SubTitle2"] = T("api.templates.delinquency_14.subtitle2") + 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["Button"] = T("api.templates.delinquency_14.button") + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") + + data.Props["Footer"] = T("api.templates.copyright") + + body, err := es.templatesContainer.RenderToString("cloud_14_day_arrears", data) + if err != nil { + return err + } + + if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail); err != nil { + return err + } + + return nil +} +func (es *Service) SendDelinquencyEmail30(email, locale, siteURL, planName string) error { + T := i18n.GetUserTranslations(locale) + + subject := T("api.templates.delinquency_30.subject", map[string]any{"Plan": planName}) + + data := es.NewEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.delinquency_30.title") + data.Props["SubTitle1"] = T("api.templates.delinquency_30.subtitle1", map[string]any{"Plan": planName}) + data.Props["SubTitle2"] = T("api.templates.delinquency_30.subtitle2") + 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["Button"] = T("api.templates.delinquency_30.button") + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") + data.Props["BulletListItems"] = []string{T("api.templates.delinquency_30.bullet.message_history"), T("api.templates.delinquency_30.bullet.files"), T("api.templates.delinquency_30.bullet.cards"), T("api.templates.delinquency_30.bullet.plugins")} + data.Props["LimitsDocs"] = T("api.templates.delinquency_30.limits_documentation") + data.Props["Footer"] = T("api.templates.copyright") + + body, err := es.templatesContainer.RenderToString("cloud_30_day_arrears", data) + if err != nil { + return err + } + + if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail); err != nil { + return err + } + + return nil +} + +func (es *Service) SendDelinquencyEmail45(email, locale, siteURL, planName, delinquencyDate string) error { + T := i18n.GetUserTranslations(locale) + + subject := T("api.templates.delinquency_45.subject", map[string]any{"Plan": planName}) + + data := es.NewEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.delinquency_45.title") + data.Props["SubTitle1"] = T("api.templates.delinquency_45.subtitle1", map[string]any{"DelinquencyDate": delinquencyDate}) + data.Props["SubTitle2"] = T("api.templates.delinquency_45.subtitle2") + data.Props["SubTitle3"] = T("api.templates.delinquency_45.subtitle3") + 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["Button"] = T("api.templates.delinquency_45.button") + data.Props["IncludeSecondaryActionButton"] = false + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") + + data.Props["Footer"] = T("api.templates.copyright") + + body, err := es.templatesContainer.RenderToString("cloud_45_day_arrears", data) + if err != nil { + return err + } + + if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail); err != nil { + return err + } + + return nil +} + +func (es *Service) SendDelinquencyEmail60(email, locale, siteURL string) error { + T := i18n.GetUserTranslations(locale) + + subject := T("api.templates.delinquency_60.subject") + + data := es.NewEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.delinquency_60.title") + data.Props["SubTitle1"] = T("api.templates.delinquency_60.subtitle1") + data.Props["SubTitle2"] = T("api.templates.delinquency_60.subtitle2") + data.Props["SubTitle3"] = T("api.templates.delinquency_60.subtitle3") + 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["Button"] = T("api.templates.delinquency_60.button") + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") + data.Props["IncludeSecondaryActionButton"] = true + data.Props["SecondaryActionButtonText"] = T("api.templates.delinquency_60.downgrade_to_starter") + data.Props["Footer"] = T("api.templates.copyright") + + // 45 day template is the same as the 60 day one so its reused + body, err := es.templatesContainer.RenderToString("cloud_45_day_arrears", data) + if err != nil { + return err + } + + if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail); err != nil { + return err + } + + return nil +} + +func (es *Service) SendDelinquencyEmail75(email, locale, siteURL, planName, delinquencyDate string) error { + T := i18n.GetUserTranslations(locale) + + subject := T("api.templates.delinquency_75.subject", map[string]any{"Plan": planName}) + + data := es.NewEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.delinquency_75.title") + data.Props["SubTitle1"] = T("api.templates.delinquency_75.subtitle1", map[string]any{"DelinquencyDate": delinquencyDate}) + data.Props["SubTitle2"] = T("api.templates.delinquency_75.subtitle2", map[string]any{"Plan": planName}) + data.Props["SubTitle3"] = T("api.templates.delinquency_75.subtitle3") + 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["Button"] = T("api.templates.delinquency_75.button") + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") + data.Props["IncludeSecondaryActionButton"] = true + data.Props["SecondaryActionButtonText"] = T("api.templates.delinquency_75.downgrade_to_starter") + data.Props["Footer"] = T("api.templates.copyright") + + // 45 day template is the same as the 75 day one so its reused + body, err := es.templatesContainer.RenderToString("cloud_45_day_arrears", data) + if err != nil { + return err + } + + if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail); err != nil { + return err + } + + return nil +} + +func (es *Service) SendDelinquencyEmail90(email, locale, siteURL string) error { + T := i18n.GetUserTranslations(locale) + + subject := T("api.templates.delinquency_90.subject") + + data := es.NewEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.delinquency_90.title") + data.Props["SubTitle1"] = T("api.templates.delinquency_90.subtitle1", map[string]any{"SiteURL": siteURL}) + data.Props["SubTitle2"] = T("api.templates.delinquency_90.subtitle2") + data.Props["SubTitle3"] = T("api.templates.delinquency_90.subtitle3") + 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["Button"] = T("api.templates.delinquency_90.button") + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") + data.Props["IncludeSecondaryActionButton"] = true + data.Props["SecondaryActionButtonText"] = T("api.templates.delinquency_90.secondary_action_button") + data.Props["Footer"] = T("api.templates.copyright") + + body, err := es.templatesContainer.RenderToString("cloud_90_day_arrears", data) + if err != nil { + return err + } + + if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail); 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(renewalLink, email string, locale, siteURL string) error { diff --git a/app/email/mocks/ServiceInterface.go b/app/email/mocks/ServiceInterface.go index 68b97109fc..fc95b9ad47 100644 --- a/app/email/mocks/ServiceInterface.go +++ b/app/email/mocks/ServiceInterface.go @@ -167,6 +167,104 @@ func (_m *ServiceInterface) SendDeactivateAccountEmail(_a0 string, locale string return r0 } +// SendDelinquencyEmail14 provides a mock function with given fields: _a0, locale, siteURL, planName +func (_m *ServiceInterface) SendDelinquencyEmail14(_a0 string, locale string, siteURL string, planName string) error { + ret := _m.Called(_a0, locale, siteURL, planName) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok { + r0 = rf(_a0, locale, siteURL, planName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendDelinquencyEmail30 provides a mock function with given fields: _a0, locale, siteURL, planName +func (_m *ServiceInterface) SendDelinquencyEmail30(_a0 string, locale string, siteURL string, planName string) error { + ret := _m.Called(_a0, locale, siteURL, planName) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok { + r0 = rf(_a0, locale, siteURL, planName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendDelinquencyEmail45 provides a mock function with given fields: _a0, locale, siteURL, planName, delinquencyDate +func (_m *ServiceInterface) SendDelinquencyEmail45(_a0 string, locale string, siteURL string, planName string, delinquencyDate string) error { + ret := _m.Called(_a0, locale, siteURL, planName, delinquencyDate) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, string, string, string) error); ok { + r0 = rf(_a0, locale, siteURL, planName, delinquencyDate) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendDelinquencyEmail60 provides a mock function with given fields: _a0, locale, siteURL +func (_m *ServiceInterface) SendDelinquencyEmail60(_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 +} + +// SendDelinquencyEmail7 provides a mock function with given fields: _a0, locale, siteURL, planName +func (_m *ServiceInterface) SendDelinquencyEmail7(_a0 string, locale string, siteURL string, planName string) error { + ret := _m.Called(_a0, locale, siteURL, planName) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok { + r0 = rf(_a0, locale, siteURL, planName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendDelinquencyEmail75 provides a mock function with given fields: _a0, locale, siteURL, planName, delinquencyDate +func (_m *ServiceInterface) SendDelinquencyEmail75(_a0 string, locale string, siteURL string, planName string, delinquencyDate string) error { + ret := _m.Called(_a0, locale, siteURL, planName, delinquencyDate) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, string, string, string) error); ok { + r0 = rf(_a0, locale, siteURL, planName, delinquencyDate) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendDelinquencyEmail90 provides a mock function with given fields: _a0, locale, siteURL +func (_m *ServiceInterface) SendDelinquencyEmail90(_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 +} + // SendEmailChangeEmail provides a mock function with given fields: oldEmail, newEmail, locale, siteURL func (_m *ServiceInterface) SendEmailChangeEmail(oldEmail string, newEmail string, locale string, siteURL string) error { ret := _m.Called(oldEmail, newEmail, locale, siteURL) diff --git a/app/email/service.go b/app/email/service.go index c4001af6be..78a7232e52 100644 --- a/app/email/service.go +++ b/app/email/service.go @@ -143,6 +143,14 @@ type ServiceInterface interface { SendMailWithEmbeddedFiles(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, messageID string, inReplyTo string, references string) error SendLicenseUpForRenewalEmail(email, name, locale, siteURL, renewalLink string, daysToExpiration int) error SendPaymentFailedEmail(email string, locale string, failedPayment *model.FailedPayment, siteURL string) (bool, error) + // Cloud delinquency email sequence + SendDelinquencyEmail7(email, locale, siteURL, planName string) error + SendDelinquencyEmail14(email, locale, siteURL, planName string) error + SendDelinquencyEmail30(email, locale, siteURL, planName string) error + SendDelinquencyEmail45(email, locale, siteURL, planName, delinquencyDate string) error + SendDelinquencyEmail60(email, locale, siteURL string) error + 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 AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 20c1ba1b8a..a02d4d443a 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -15084,6 +15084,28 @@ func (a *OpenTracingAppLayer) SendAutoResponseIfNecessary(c request.CTX, channel return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) SendDelinquencyEmail(emailToSend model.DelinquencyEmail) *model.AppError { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendDelinquencyEmail") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.SendDelinquencyEmail(emailToSend) + + if resultVar0 != nil { + span.LogFields(spanlog.Error(resultVar0)) + ext.Error.Set(span, true) + } + + return resultVar0 +} + func (a *OpenTracingAppLayer) SendEmailVerification(user *model.User, newEmail string, redirect string) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendEmailVerification") diff --git a/i18n/en.json b/i18n/en.json index 3873589405..31cfab8f01 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -471,6 +471,10 @@ "id": "api.cloud.cws_webhook_event_missing_error", "translation": "Webhook event was not handled. Either it is missing or it is not valid." }, + { + "id": "api.cloud.delinquency_email.missing_email_to_trigger", + "translation": "Missing required fields to send delinquency email." + }, { "id": "api.cloud.license_error", "translation": "Your license does not support cloud requests." @@ -3247,6 +3251,190 @@ "id": "api.templates.deactivate_subject", "translation": "[{{ .SiteName }}] Your account at {{ .ServerURL }} has been deactivated" }, + { + "id": "api.templates.delinquency_14.button", + "translation": "Update payment" + }, + { + "id": "api.templates.delinquency_14.subject", + "translation": "Payment is overdue for your Mattermost {{.Plan}}." + }, + { + "id": "api.templates.delinquency_14.subtitle1", + "translation": "We weren't able to charge the credit card we have on file. This means your workspace is at risk of being downgraded to Cloud Starter." + }, + { + "id": "api.templates.delinquency_14.subtitle2", + "translation": "Please contact your financial institution to resolve any issues. Then, update your payment details as needed." + }, + { + "id": "api.templates.delinquency_14.title", + "translation": "Payment not received" + }, + { + "id": "api.templates.delinquency_30.bullet.cards", + "translation": "Cards from your Boards" + }, + { + "id": "api.templates.delinquency_30.bullet.files", + "translation": "Files" + }, + { + "id": "api.templates.delinquency_30.bullet.message_history", + "translation": "Message history" + }, + { + "id": "api.templates.delinquency_30.bullet.plugins", + "translation": "Active plugins and integrations" + }, + { + "id": "api.templates.delinquency_30.button", + "translation": "Update payment" + }, + { + "id": "api.templates.delinquency_30.limits_documentation", + "translation": "View all limits documentation." + }, + { + "id": "api.templates.delinquency_30.subject", + "translation": "Act to keep your Mattermost {{.Plan}} Features" + }, + { + "id": "api.templates.delinquency_30.subtitle1", + "translation": "You have time to keep your Mattermost {{.Plan}} active but you'll need to resolve issues with your payment method." + }, + { + "id": "api.templates.delinquency_30.subtitle2", + "translation": "if no action is taken, your workspace will be downgraded and the following data may be archived:" + }, + { + "id": "api.templates.delinquency_30.title", + "translation": "Your workspace will be downgraded soon" + }, + { + "id": "api.templates.delinquency_45.button", + "translation": "Update payment" + }, + { + "id": "api.templates.delinquency_45.subject", + "translation": "Notice: Your Mattermost {{.Plan}} will be downgraded soon" + }, + { + "id": "api.templates.delinquency_45.subtitle1", + "translation": "We've been unable to collect payment for outstanding invoices dated {{.DelinquencyDate}}. Your workspace is at risk of being downgraded." + }, + { + "id": "api.templates.delinquency_45.subtitle2", + "translation": "A downgraded workspace might negatively affect critical workflows, integrations and other business critical activities carried at your workspace." + }, + { + "id": "api.templates.delinquency_45.subtitle3", + "translation": "Update your credit card information now." + }, + { + "id": "api.templates.delinquency_45.title", + "translation": "Your workspace will be downgraded soon" + }, + { + "id": "api.templates.delinquency_60.button", + "translation": "Update payment" + }, + { + "id": "api.templates.delinquency_60.downgrade_to_starter", + "translation": "Downgrade to Cloud Starter" + }, + { + "id": "api.templates.delinquency_60.subject", + "translation": "Action Required: Workspace will be downgraded in 30 days" + }, + { + "id": "api.templates.delinquency_60.subtitle1", + "translation": "Please update your payment information soon to process your outstanding invoices." + }, + { + "id": "api.templates.delinquency_60.subtitle2", + "translation": "We will downgrade your workspace automatically in 30 days if we are unable to process your payment." + }, + { + "id": "api.templates.delinquency_60.subtitle3", + "translation": "Update your payment information now or downgrade to Cloud Starter below." + }, + { + "id": "api.templates.delinquency_60.title", + "translation": "Your Mattermost workspace will be downgraded in 30 days" + }, + { + "id": "api.templates.delinquency_7.button", + "translation": "Update payment" + }, + { + "id": "api.templates.delinquency_7.subtitle1", + "translation": "We couldn't process your most recent payment" + }, + { + "id": "api.templates.delinquency_7.subtitle2", + "translation": "To keep your {{.Plan}} plan active, please contact your financial institution as soon as possible. Then, update your payment details as needed." + }, + { + "id": "api.templates.delinquency_7.title", + "translation": "Your payment wasn't completed" + }, + { + "id": "api.templates.delinquency_75.button", + "translation": "Update payment" + }, + { + "id": "api.templates.delinquency_75.downgrade_to_starter", + "translation": "Downgrade to Cloud Starter" + }, + { + "id": "api.templates.delinquency_75.subject", + "translation": "Your Mattermost {{.Plan}} will be downgraded in 15 days" + }, + { + "id": "api.templates.delinquency_75.subtitle1", + "translation": "This is a final reminder that we haven’t received payment for your Mattermost Cloud workspace since {{.DelinquencyDate}}" + }, + { + "id": "api.templates.delinquency_75.subtitle2", + "translation": "Your workspace will be downgraded to Cloud Starter. Your {{.Plan}} features will be locked and some of your workspace data may be archived until your full outstanding balance is settled." + }, + { + "id": "api.templates.delinquency_75.subtitle3", + "translation": "Update your payment information now, or downgrade to Cloud Starter." + }, + { + "id": "api.templates.delinquency_75.title", + "translation": "Your workspace will be downgraded in 15 days" + }, + { + "id": "api.templates.delinquency_90.button", + "translation": "Update payment" + }, + { + "id": "api.templates.delinquency_90.secondary_action_button", + "translation": "View Plans & Pricing" + }, + { + "id": "api.templates.delinquency_90.subject", + "translation": "Your Mattermost Cloud workspace has been downgraded" + }, + { + "id": "api.templates.delinquency_90.subtitle1", + "translation": "If you use Cloud Professional or Enterprise features for important business operations, these will no longer be available and you'll experience degraded performance." + }, + { + "id": "api.templates.delinquency_90.subtitle2", + "translation": "In addition, your data may have been archived due to Cloud Starter limitations." + }, + { + "id": "api.templates.delinquency_90.subtitle3", + "translation": "To unarchive your data and keep paid features, update your payment information." + }, + { + "id": "api.templates.delinquency_90.title", + "translation": "Your Mattermost workspace has been downgraded" + }, { "id": "api.templates.email_change_body.info", "translation": "Your email address for {{.TeamDisplayName}} has been changed to {{.NewEmail}}." @@ -4651,6 +4839,18 @@ "id": "app.channel_member_history.log_leave_event.internal_error", "translation": "Failed to record channel member history. Failed to update existing join record" }, + { + "id": "app.cloud.get_cloud_products.app_error", + "translation": "Couldn't retrieve cloud products" + }, + { + "id": "app.cloud.get_subscription.app_error", + "translation": "Couldn't retrieve cloud subscription" + }, + { + "id": "app.cloud.get_subscription_delinquency_date.app_error", + "translation": "Subscription is not delinquent" + }, { "id": "app.command.createcommand.internal_error", "translation": "Unable to save the command." diff --git a/model/cloud.go b/model/cloud.go index 8aa16ebc2e..940da261e8 100644 --- a/model/cloud.go +++ b/model/cloud.go @@ -15,6 +15,7 @@ const ( EventTypeSendAdminWelcomeEmail = "send-admin-welcome-email" EventTypeSendUpgradeConfirmationEmail = "send-upgrade-confirmation-email" EventTypeSubscriptionChanged = "subscription-changed" + EventTypeTriggerDelinquencyEmail = "trigger-delinquency-email" ) var MockCWS string @@ -189,13 +190,30 @@ type InvoiceLineItem struct { Metadata map[string]any `json:"metadata"` } +type DelinquencyEmailTrigger struct { + EmailToTrigger string `json:"email_to_send"` +} + +type DelinquencyEmail string + +const ( + DelinquencyEmail7 DelinquencyEmail = "7" + DelinquencyEmail14 DelinquencyEmail = "14" + DelinquencyEmail30 DelinquencyEmail = "30" + DelinquencyEmail45 DelinquencyEmail = "45" + DelinquencyEmail60 DelinquencyEmail = "60" + DelinquencyEmail75 DelinquencyEmail = "75" + DelinquencyEmail90 DelinquencyEmail = "90" +) + type CWSWebhookPayload struct { - Event string `json:"event"` - FailedPayment *FailedPayment `json:"failed_payment"` - CloudWorkspaceOwner *CloudWorkspaceOwner `json:"cloud_workspace_owner"` - ProductLimits *ProductLimits `json:"product_limits"` - Subscription *Subscription `json:"subscription"` - SubscriptionTrialEndUnixTimeStamp int64 `json:"trial_end_time_stamp"` + Event string `json:"event"` + FailedPayment *FailedPayment `json:"failed_payment"` + CloudWorkspaceOwner *CloudWorkspaceOwner `json:"cloud_workspace_owner"` + ProductLimits *ProductLimits `json:"product_limits"` + Subscription *Subscription `json:"subscription"` + SubscriptionTrialEndUnixTimeStamp int64 `json:"trial_end_time_stamp"` + DelinquencyEmail *DelinquencyEmailTrigger `json:"delinquency_email"` } type FailedPayment struct { diff --git a/templates/cloud_14_day_arrears.html b/templates/cloud_14_day_arrears.html new file mode 100644 index 0000000000..85f7643376 --- /dev/null +++ b/templates/cloud_14_day_arrears.html @@ -0,0 +1,582 @@ +{{define "cloud_14_day_arrears"}} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + +
+
{{.Props.Title}}
+
+
{{.Props.SubTitle1}}
+
+
{{.Props.SubTitle2}}
+
+ + + + +
+ + {{.Props.Button}} + +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ +
+
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + + + +
+
{{.Props.QuestionTitle}}
+
+
{{.Props.QuestionInfo}} + + {{.Props.SupportEmail}} +
+
+
+
+ +
+ + + + + + +
+ + + + + + +
+

+

+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+
{{.Props.Organization}} + {{.Props.FooterV2}} +
+
+
+
+ +
+
+ +
+
+ +
+ + + + +{{end}} diff --git a/templates/cloud_14_day_arrears.mjml b/templates/cloud_14_day_arrears.mjml new file mode 100644 index 0000000000..1c6617ca81 --- /dev/null +++ b/templates/cloud_14_day_arrears.mjml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/templates/cloud_30_day_arrears.html b/templates/cloud_30_day_arrears.html new file mode 100644 index 0000000000..2388bc3b67 --- /dev/null +++ b/templates/cloud_30_day_arrears.html @@ -0,0 +1,608 @@ +{{define "cloud_30_day_arrears"}} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+
{{.Props.Title}}
+
+
{{.Props.SubTitle1}}
+
+
{{.Props.SubTitle2}}
+
+
+ +
    + {{ range .Props.BulletListItems}} +
  • {{.}}
  • + {{end}} +
+
+
+
+ + + + +
+ + {{.Props.LimitsDocs}} + +
+
+ + + + +
+ + {{.Props.Button}} + +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ +
+
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + + + +
+
{{.Props.QuestionTitle}}
+
+
{{.Props.QuestionInfo}} + + {{.Props.SupportEmail}} +
+
+
+
+ +
+ + + + + + +
+ + + + + + +
+

+

+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+
{{.Props.Organization}} + {{.Props.FooterV2}} +
+
+
+
+ +
+
+ +
+
+ +
+ + + + +{{end}} diff --git a/templates/cloud_30_day_arrears.mjml b/templates/cloud_30_day_arrears.mjml new file mode 100644 index 0000000000..8f1fff3ca1 --- /dev/null +++ b/templates/cloud_30_day_arrears.mjml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + {{.Props.Title}} + + + {{.Props.SubTitle1}} + + + {{.Props.SubTitle2}} + + + +
    + {{ range .Props.BulletListItems}} +
  • {{.}}
  • + {{end}} +
+
+
+ + {{.Props.LimitsDocs}} + + + {{.Props.Button}} + +
+
+ + +
+
+
diff --git a/templates/cloud_45_day_arrears.html b/templates/cloud_45_day_arrears.html new file mode 100644 index 0000000000..97df400064 --- /dev/null +++ b/templates/cloud_45_day_arrears.html @@ -0,0 +1,600 @@ +{{define "cloud_45_day_arrears"}} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+
{{.Props.Title}}
+
+
{{.Props.SubTitle1}}
+
+
{{.Props.SubTitle2}}
+
+
{{.Props.SubTitle3}}
+
+ + + + +
+ + {{.Props.Button}} + +
+
+ + + + +
+ + {{.Props.SecondaryActionButtonText}} + +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ +
+
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + + + +
+
{{.Props.QuestionTitle}}
+
+
{{.Props.QuestionInfo}} + + {{.Props.SupportEmail}} +
+
+
+
+ +
+ + + + + + +
+ + + + + + +
+

+

+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+
{{.Props.Organization}} + {{.Props.FooterV2}} +
+
+
+
+ +
+
+ +
+
+ +
+ + + + +{{end}} diff --git a/templates/cloud_45_day_arrears.mjml b/templates/cloud_45_day_arrears.mjml new file mode 100644 index 0000000000..3f4799fd04 --- /dev/null +++ b/templates/cloud_45_day_arrears.mjml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/templates/cloud_7_day_arrears.html b/templates/cloud_7_day_arrears.html new file mode 100644 index 0000000000..db1d3447bd --- /dev/null +++ b/templates/cloud_7_day_arrears.html @@ -0,0 +1,582 @@ +{{define "cloud_7_day_arrears"}} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + +
+
{{.Props.Title}}
+
+
{{.Props.SubTitle1}}
+
+
{{.Props.SubTitle2}}
+
+ + + + +
+ + {{.Props.Button}} + +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ +
+
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + + + +
+
{{.Props.QuestionTitle}}
+
+
{{.Props.QuestionInfo}} + + {{.Props.SupportEmail}} +
+
+
+
+ +
+ + + + + + +
+ + + + + + +
+

+

+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+
{{.Props.Organization}} + {{.Props.FooterV2}} +
+
+
+
+ +
+
+ +
+
+ +
+ + + + +{{end}} diff --git a/templates/cloud_7_day_arrears.mjml b/templates/cloud_7_day_arrears.mjml new file mode 100644 index 0000000000..1c6617ca81 --- /dev/null +++ b/templates/cloud_7_day_arrears.mjml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/templates/cloud_90_day_arrears.html b/templates/cloud_90_day_arrears.html new file mode 100644 index 0000000000..5832fd1538 --- /dev/null +++ b/templates/cloud_90_day_arrears.html @@ -0,0 +1,600 @@ +{{define "cloud_90_day_arrears"}} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+
{{.Props.Title}}
+
+
{{.Props.SubTitle1}}
+
+
{{.Props.SubTitle2}}
+
+
{{.Props.SubTitle3}}
+
+ + + + +
+ + {{.Props.Button}} + +
+
+ + + + +
+ + {{.Props.SecondaryActionButtonText}} + +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ +
+
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + + + +
+
{{.Props.QuestionTitle}}
+
+
{{.Props.QuestionInfo}} + + {{.Props.SupportEmail}} +
+
+
+
+ +
+ + + + + + +
+ + + + + + +
+

+

+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+
{{.Props.Organization}} + {{.Props.FooterV2}} +
+
+
+
+ +
+
+ +
+
+ +
+ + + + +{{end}} diff --git a/templates/cloud_90_day_arrears.mjml b/templates/cloud_90_day_arrears.mjml new file mode 100644 index 0000000000..be785ed1d1 --- /dev/null +++ b/templates/cloud_90_day_arrears.mjml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/templates/cloud_upgrade_confirmation.html b/templates/cloud_upgrade_confirmation.html index 7bd9b57765..49d53b82c2 100644 --- a/templates/cloud_upgrade_confirmation.html +++ b/templates/cloud_upgrade_confirmation.html @@ -98,7 +98,7 @@ .emailBody a { text-decoration: none !important; - color: #1C58D9 !important; + color: #1C58D9; } .title div { @@ -445,7 +445,7 @@
{{.Props.QuestionInfo}} - + {{.Props.SupportEmail}}
diff --git a/templates/inactivity_body.html b/templates/inactivity_body.html index 266da3b339..e742a9e81e 100644 --- a/templates/inactivity_body.html +++ b/templates/inactivity_body.html @@ -98,7 +98,7 @@ .emailBody a { text-decoration: none !important; - color: #1C58D9 !important; + color: #1C58D9; } .title div { @@ -481,7 +481,7 @@
{{.Props.QuestionInfo}} - + {{.Props.SupportEmail}}
diff --git a/templates/invite_body.html b/templates/invite_body.html index 0866ebd594..730240f958 100644 --- a/templates/invite_body.html +++ b/templates/invite_body.html @@ -118,7 +118,7 @@ .emailBody a { text-decoration: none !important; - color: #1C58D9 !important; + color: #1C58D9; } .title div { diff --git a/templates/license_up_for_renewal.html b/templates/license_up_for_renewal.html index aa455ddc98..f91f3c2b4d 100644 --- a/templates/license_up_for_renewal.html +++ b/templates/license_up_for_renewal.html @@ -98,7 +98,7 @@ .emailBody a { text-decoration: none !important; - color: #1C58D9 !important; + color: #1C58D9; } .title div { @@ -470,7 +470,7 @@
{{.Props.QuestionInfo}} - + {{.Props.SupportEmail}}
diff --git a/templates/messages_notification.html b/templates/messages_notification.html index 6eff990157..f36e8e986d 100644 --- a/templates/messages_notification.html +++ b/templates/messages_notification.html @@ -118,7 +118,7 @@ .emailBody a { text-decoration: none !important; - color: #1C58D9 !important; + color: #1C58D9; } .title div { diff --git a/templates/partials/cloud_laptop_error.mjml b/templates/partials/cloud_laptop_error.mjml new file mode 100644 index 0000000000..d184f38011 --- /dev/null +++ b/templates/partials/cloud_laptop_error.mjml @@ -0,0 +1,5 @@ + + + + + diff --git a/templates/partials/cloud_laptop_warning.mjml b/templates/partials/cloud_laptop_warning.mjml new file mode 100644 index 0000000000..e205abbc5d --- /dev/null +++ b/templates/partials/cloud_laptop_warning.mjml @@ -0,0 +1,5 @@ + + + + + diff --git a/templates/partials/cloud_questions_hr_email_footer.mjml b/templates/partials/cloud_questions_hr_email_footer.mjml new file mode 100644 index 0000000000..f8bf2890d3 --- /dev/null +++ b/templates/partials/cloud_questions_hr_email_footer.mjml @@ -0,0 +1,23 @@ + + + + {{.Props.QuestionTitle}} + + + {{.Props.QuestionInfo}} + + {{.Props.SupportEmail}} + + + + + + + + + + {{.Props.Organization}} + {{.Props.FooterV2}} + + + diff --git a/templates/partials/cloud_title_2subtitles_button.mjml b/templates/partials/cloud_title_2subtitles_button.mjml new file mode 100644 index 0000000000..6f1f2bb22b --- /dev/null +++ b/templates/partials/cloud_title_2subtitles_button.mjml @@ -0,0 +1,16 @@ + + + + {{.Props.Title}} + + + {{.Props.SubTitle1}} + + + {{.Props.SubTitle2}} + + + {{.Props.Button}} + + + diff --git a/templates/partials/cloud_title_3subtitles_button.mjml b/templates/partials/cloud_title_3subtitles_button.mjml new file mode 100644 index 0000000000..6e41aef92f --- /dev/null +++ b/templates/partials/cloud_title_3subtitles_button.mjml @@ -0,0 +1,24 @@ + + + + {{.Props.Title}} + + + {{.Props.SubTitle1}} + + + {{.Props.SubTitle2}} + + + {{.Props.SubTitle3}} + + + {{.Props.Button}} + + {{if .IncludeSecondaryActionButton}} + + {{.Props.SecondaryActionButtonText}} + + {{end}} + + diff --git a/templates/partials/cloud_upgrade_person.mjml b/templates/partials/cloud_upgrade_person.mjml new file mode 100644 index 0000000000..ecd94d8735 --- /dev/null +++ b/templates/partials/cloud_upgrade_person.mjml @@ -0,0 +1,5 @@ + + + + + diff --git a/templates/partials/style.css b/templates/partials/style.css index 0e303632a4..f353ecf4a6 100644 --- a/templates/partials/style.css +++ b/templates/partials/style.css @@ -6,7 +6,7 @@ .emailBody a{ text-decoration: none !important; - color: #1C58D9 !important; + color: #1C58D9; } .title div { diff --git a/templates/reset_body.html b/templates/reset_body.html index 740e9574eb..3f1d3c95ce 100644 --- a/templates/reset_body.html +++ b/templates/reset_body.html @@ -98,7 +98,7 @@ .emailBody a { text-decoration: none !important; - color: #1C58D9 !important; + color: #1C58D9; } .title div { diff --git a/templates/verify_body.html b/templates/verify_body.html index 27811f5a82..8f0acf64cc 100644 --- a/templates/verify_body.html +++ b/templates/verify_body.html @@ -98,7 +98,7 @@ .emailBody a { text-decoration: none !important; - color: #1C58D9 !important; + color: #1C58D9; } .title div { diff --git a/templates/welcome_body.html b/templates/welcome_body.html index 91775108cb..4954cf7b85 100644 --- a/templates/welcome_body.html +++ b/templates/welcome_body.html @@ -98,7 +98,7 @@ .emailBody a { text-decoration: none !important; - color: #1C58D9 !important; + color: #1C58D9; } .title div {