From fa5a033f66f46eeeb1d1387e9fd02f0a289bf1bb Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Thu, 3 Dec 2020 16:02:43 -0500 Subject: [PATCH] [MM-30984] Missing payment email on billing day if no CC is present (#16442) * Adding email and scaffolding for payment failure in case where customer has not added payment method * Adding email template * Remove unused boolean * Fix error * Add Email Us verbiage Co-authored-by: Mattermod --- api4/cloud.go | 6 +- app/app_iface.go | 2 + app/cloud.go | 16 ++++ app/email.go | 21 +++++ app/opentracing/opentracing_layer.go | 22 ++++++ i18n/en.json | 20 +++++ model/cloud.go | 3 +- templates/payment_failed_no_card_body.html | 92 ++++++++++++++++++++++ 8 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 templates/payment_failed_no_card_body.html diff --git a/api4/cloud.go b/api4/cloud.go index 93ca4c4a22..acd629e60d 100644 --- a/api4/cloud.go +++ b/api4/cloud.go @@ -353,7 +353,11 @@ func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = nErr return } - + case model.EventTypeFailedPaymentNoCard: + if nErr := c.App.SendNoCardPaymentFailedEmail(); nErr != nil { + c.Err = nErr + return + } } ReturnStatusOK(w) diff --git a/app/app_iface.go b/app/app_iface.go index f16eeb87ef..a83f7f0acc 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -265,6 +265,8 @@ type AppIface interface { SearchAllChannels(term string, opts model.ChannelSearchOpts) (*model.ChannelListWithTeamData, int64, *model.AppError) // SearchAllTeams returns a team list and the total count of the results SearchAllTeams(searchOpts *model.TeamSearch) ([]*model.Team, int64, *model.AppError) + // SendNoCardPaymentFailedEmail + SendNoCardPaymentFailedEmail() *model.AppError // ServePluginPublicRequest serves public plugin files // at the URL http(s)://$SITE_URL/plugins/$PLUGIN_ID/public/{anything} ServePluginPublicRequest(w http.ResponseWriter, r *http.Request) diff --git a/app/cloud.go b/app/cloud.go index a34799d0f3..bf79a52e8f 100644 --- a/app/cloud.go +++ b/app/cloud.go @@ -75,3 +75,19 @@ func (a *App) SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model. } return nil } + +// SendNoCardPaymentFailedEmail +func (a *App) SendNoCardPaymentFailedEmail() *model.AppError { + sysAdmins, err := a.getSysAdminsEmailRecipients() + if err != nil { + return err + } + + for _, admin := range sysAdmins { + err := a.Srv().EmailService.SendNoCardPaymentFailedEmail(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL) + if err != nil { + a.Log().Error("Error sending payment failed email", mlog.Err(err)) + } + } + return nil +} diff --git a/app/email.go b/app/email.go index 2236f5ac04..fe4a44c7e5 100644 --- a/app/email.go +++ b/app/email.go @@ -779,6 +779,7 @@ func (es *EmailService) SendPaymentFailedEmail(email string, locale string, fail bodyPage.Props["Info2"] = T("api.templates.payment_failed.info2") bodyPage.Props["Info3"] = T("api.templates.payment_failed.info3") bodyPage.Props["Button"] = T("api.templates.over_limit_fix_now") + bodyPage.Props["EmailUs"] = T("api.templates.email_us_anytime_at") bodyPage.Props["FailedReason"] = failedPayment.FailureMessage @@ -788,3 +789,23 @@ func (es *EmailService) SendPaymentFailedEmail(email string, locale string, fail return true, nil } + +func (es *EmailService) SendNoCardPaymentFailedEmail(email string, locale string, siteURL string) *model.AppError { + T := utils.GetUserTranslations(locale) + + subject := T("api.templates.payment_failed_no_card.subject") + + bodyPage := es.newEmailTemplate("payment_failed_no_card_body", locale) + bodyPage.Props["SiteURL"] = siteURL + bodyPage.Props["Title"] = T("api.templates.payment_failed_no_card.title") + bodyPage.Props["Info1"] = T("api.templates.payment_failed_no_card.info1") + bodyPage.Props["Info3"] = T("api.templates.payment_failed_no_card.info3") + bodyPage.Props["Button"] = T("api.templates.payment_failed_no_card.button") + bodyPage.Props["EmailUs"] = T("api.templates.email_us_anytime_at") + + if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + return model.NewAppError("SendPaymentFailedEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Message, http.StatusInternalServerError) + } + + return nil +} diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index f033a9e9da..1fb2f8747f 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -13110,6 +13110,28 @@ func (a *OpenTracingAppLayer) SendEphemeralPost(userId string, post *model.Post) return resultVar0 } +func (a *OpenTracingAppLayer) SendNoCardPaymentFailedEmail() *model.AppError { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendNoCardPaymentFailedEmail") + + 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.SendNoCardPaymentFailedEmail() + + if resultVar0 != nil { + span.LogFields(spanlog.Error(resultVar0)) + ext.Error.Set(span, true) + } + + return resultVar0 +} + func (a *OpenTracingAppLayer) SendNotifications(post *model.Post, team *model.Team, channel *model.Channel, sender *model.User, parentPostList *model.PostList, setOnline bool) ([]string, error) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendNotifications") diff --git a/i18n/en.json b/i18n/en.json index f8cfbe6304..2513fe5f30 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2882,6 +2882,26 @@ "id": "api.templates.payment_failed.title", "translation": "Failed Payment" }, + { + "id": "api.templates.payment_failed_no_card.button", + "translation": "Pay now" + }, + { + "id": "api.templates.payment_failed_no_card.info1", + "translation": "Your Mattermost Cloud invoice for the most recent billing period has been processed. However, we don't have your payment details on file." + }, + { + "id": "api.templates.payment_failed_no_card.info3", + "translation": "To review your invoice and add a payment method, select Pay now." + }, + { + "id": "api.templates.payment_failed_no_card.subject", + "translation": "Payment is due for your Mattermost Cloud subscription" + }, + { + "id": "api.templates.payment_failed_no_card.title", + "translation": "Your Mattermost Cloud Invoice is due" + }, { "id": "api.templates.post_body.button", "translation": "Go To Post" diff --git a/model/cloud.go b/model/cloud.go index efd89762f7..21402895fd 100644 --- a/model/cloud.go +++ b/model/cloud.go @@ -4,7 +4,8 @@ package model const ( - EventTypeFailedPayment = "failed-payment" + EventTypeFailedPayment = "failed-payment" + EventTypeFailedPaymentNoCard = "failed-payment-no-card" ) // Product model represents a product on the cloud system. diff --git a/templates/payment_failed_no_card_body.html b/templates/payment_failed_no_card_body.html new file mode 100644 index 0000000000..445752025e --- /dev/null +++ b/templates/payment_failed_no_card_body.html @@ -0,0 +1,92 @@ +{{define "payment_failed_no_card_body"}} + + + + + +
+ + + + +
+ + + + +
+ +
+ + + + +
+ + + + +
+ + + + +
+

+ {{ .Props.Title }}

+
+

+ {{ .Props.Info1 }}

+

{{.Props.Info3}}

+

+ {{ .Props.Button }} +

+
+
+ + + + +
+ + + + +
+

Questions?

+

{{ .Props.EmailUs }} feedback@mattermost.com

+
+ + + + +
+ + + + + +
+

+ {{.Props.Organization}}
+ {{.Props.Footer}} +

+
+
+
+ +{{end}}