[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 <mattermod@users.noreply.github.com>
Этот коммит содержится в:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
16
app/cloud.go
16
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
|
||||
}
|
||||
|
||||
21
app/email.go
21
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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
20
i18n/en.json
20
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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
92
templates/payment_failed_no_card_body.html
Обычный файл
92
templates/payment_failed_no_card_body.html
Обычный файл
@@ -0,0 +1,92 @@
|
||||
{{define "payment_failed_no_card_body"}}
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="margin-top: 20px; line-height: 1.7; color: #555;font-family: Arial; font-style: normal; font-weight: bold;">
|
||||
<tr>
|
||||
<td>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;">
|
||||
<tr>
|
||||
<td style="border: 1px solid #ddd;">
|
||||
<table align="left" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px 20px 10px; text-align:left;">
|
||||
<img src="{{.Props.SiteURL}}/static/images/logo-email.png" style="opacity: 0.5"
|
||||
width="130px" alt="">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="border-collapse: collapse; max-width: 443px">
|
||||
<tr style="width: 443px">
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0"
|
||||
style="padding: 20px 0 0; text-align: center; margin: 0 auto">
|
||||
<tr>
|
||||
<td style="padding: 0 0 64px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0"
|
||||
width="371px">
|
||||
<tr>
|
||||
<td>
|
||||
<h2
|
||||
style="margin-bottom: 0; text-align: center; color: #3D3C40; font-weight: bold; margin-top: 10px; line-height: 32px; font-size: 28px; font-family: Arial">
|
||||
{{ .Props.Title }}</h2>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p
|
||||
style="font-weight: normal; color: #3D3C40; font-size: 16px; line-height: 24px; font-family: Arial">
|
||||
{{ .Props.Info1 }}</p>
|
||||
<p style="margin-top: 24px; margin-bottom: 24px; font-weight: normal; font-size: 16px; line-height: 24px; color: #3D3C40; font-family: Arial">{{.Props.Info3}}</p>
|
||||
<p style="margin: 31px 0 31px">
|
||||
<a href="{{.Props.SiteURL}}/admin_console/billing/subscription"
|
||||
target="_blank"
|
||||
style="font-weight: normal; border-radius: 4px; background: #166DE0; color: #fff;outline: none; min-width: 200px; padding: 11px 24px 11px 24px; font-size: 16px; font-family: Arial; cursor: pointer; -webkit-appearance: none;text-decoration: none; margin-top: 16px;">{{ .Props.Button }}</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspcing="0" width="612px">
|
||||
<tr>
|
||||
<td style="border-bottom: 1px solid #E5E5E5"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="left" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="max-width: 443px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="font-family: Arial; padding: 20px 20px 24px 48px; text-align:left;">
|
||||
<h3 style="margin-bottom: 0;">Questions?</h3>
|
||||
<p style="font-weight: normal; margin-top: 0;">{{ .Props.EmailUs }} <a
|
||||
href="mailto:feedback@mattermost.com">feedback@mattermost.com</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspcing="0" width="612px">
|
||||
<tr>
|
||||
<td style="border-bottom: 1px solid #E5E5E5"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="font-family: Arial; line-height: 16px; font-size: 12px; background: #FFF;">
|
||||
<tr>
|
||||
<td
|
||||
style="font-weight: normal; text-align: center;color: #AAA;font-size: 11px;padding-bottom: 10px;padding: 0;line-height: 16px;padding-bottom: 48px;padding-top: 4px;">
|
||||
<p>
|
||||
{{.Props.Organization}}<br>
|
||||
{{.Props.Footer}}
|
||||
</p>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{end}}
|
||||
Ссылка в новой задаче
Block a user