1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-02 23:41:21 +01:00

subscription renewal reminder emails

This commit is contained in:
Kyle Spearrin 2018-05-11 08:29:23 -04:00
parent 4e6e215d35
commit 053096c1a1
16 changed files with 199 additions and 29 deletions

View File

@ -5,6 +5,7 @@
<TargetFramework>netcoreapp2.0</TargetFramework>
<RootNamespace>Bit.Billing</RootNamespace>
<UserSecretsId>bitwarden-Billing</UserSecretsId>
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
</PropertyGroup>
<ItemGroup>

View File

@ -1,4 +1,6 @@
using Bit.Core.Services;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
@ -17,18 +19,24 @@ namespace Bit.Billing.Controllers
private readonly BillingSettings _billingSettings;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IOrganizationService _organizationService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IUserService _userService;
private readonly IMailService _mailService;
public StripeController(
IOptions<BillingSettings> billingSettings,
IHostingEnvironment hostingEnvironment,
IOrganizationService organizationService,
IUserService userService)
IOrganizationRepository organizationRepository,
IUserService userService,
IMailService mailService)
{
_billingSettings = billingSettings?.Value;
_hostingEnvironment = hostingEnvironment;
_organizationService = organizationService;
_organizationRepository = organizationRepository;
_userService = userService;
_mailService = mailService;
}
[HttpPost("webhook")]
@ -105,7 +113,7 @@ namespace Bit.Billing.Controllers
}
}
}
else if(false /* Disabled for now */ && invUpcoming)
else if(invUpcoming)
{
StripeInvoice invoice = Mapper<StripeInvoice>.MapFromJson(
parsedEvent.Data.Object.ToString());
@ -114,7 +122,6 @@ namespace Bit.Billing.Controllers
throw new Exception("Invoice is null.");
}
// TODO: maybe invoice subscription expandable is already here any we don't need to call API?
var subscriptionService = new StripeSubscriptionService();
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
if(subscription == null)
@ -122,21 +129,32 @@ namespace Bit.Billing.Controllers
throw new Exception("Invoice subscription is null.");
}
string email = null;
var ids = GetIdsFromMetaData(subscription.Metadata);
// To include in email:
// invoice.AmountDue;
// invoice.DueDate;
// org
if(ids.Item1.HasValue)
{
// TODO: email billing contact
var org = await _organizationRepository.GetByIdAsync(ids.Item1.Value);
if(org != null && OrgPlanForInvoiceNotifications(org))
{
email = org.BillingEmail;
}
}
// user
else if(ids.Item2.HasValue)
{
// TODO: email user
var user = await _userService.GetUserByIdAsync(ids.Item2.Value);
if(user.Premium)
{
email = user.Email;
}
}
if(!string.IsNullOrWhiteSpace(email) && invoice.NextPaymentAttempt.HasValue)
{
var items = invoice.StripeInvoiceLineItems.Select(i => i.Description).ToList();
await _mailService.SendInvoiceUpcomingAsync(email, invoice.AmountDue / 100M,
invoice.NextPaymentAttempt.Value, items, ids.Item1.HasValue);
}
}
@ -181,5 +199,18 @@ namespace Bit.Billing.Controllers
return new Tuple<Guid?, Guid?>(orgId, userId);
}
private bool OrgPlanForInvoiceNotifications(Organization org)
{
switch(org.PlanType)
{
case Core.Enums.PlanType.FamiliesAnnually:
case Core.Enums.PlanType.TeamsAnnually:
case Core.Enums.PlanType.EnterpriseAnnually:
return true;
default:
return false;
}
}
}
}

View File

@ -7,7 +7,10 @@
</PropertyGroup>
<ItemGroup>
<None Remove="MailTemplates\Markdown\InvoiceUpcoming.md" />
<None Remove="MailTemplates\Markdown\PasswordlessSignIn.md" />
<None Remove="MailTemplates\Razor\InvoiceUpcoming.cshtml" />
<None Remove="MailTemplates\Razor\InvoiceUpcoming.text.cshtml" />
<None Remove="MailTemplates\Razor\PasswordlessSignIn.cshtml" />
<None Remove="MailTemplates\Razor\PasswordlessSignIn.text.cshtml" />
</ItemGroup>
@ -18,6 +21,7 @@
<EmbeddedResource Include="MailTemplates\Markdown\ChangeEmailAlreadyExists.md" />
<EmbeddedResource Include="MailTemplates\Markdown\MasterPasswordHint.md" />
<EmbeddedResource Include="MailTemplates\Markdown\NoMasterPasswordHint.md" />
<EmbeddedResource Include="MailTemplates\Markdown\InvoiceUpcoming.md" />
<EmbeddedResource Include="MailTemplates\Markdown\OrganizationUserAccepted.md" />
<EmbeddedResource Include="MailTemplates\Markdown\OrganizationUserConfirmed.md" />
<EmbeddedResource Include="MailTemplates\Markdown\PasswordlessSignIn.md" />
@ -26,6 +30,8 @@
<EmbeddedResource Include="MailTemplates\Markdown\VerifyDelete.md" />
<EmbeddedResource Include="MailTemplates\Markdown\VerifyEmail.md" />
<EmbeddedResource Include="MailTemplates\Markdown\Welcome.md" />
<EmbeddedResource Include="MailTemplates\Razor\InvoiceUpcoming.cshtml" />
<EmbeddedResource Include="MailTemplates\Razor\InvoiceUpcoming.text.cshtml" />
<EmbeddedResource Include="MailTemplates\Razor\PasswordlessSignIn.text.cshtml" />
<EmbeddedResource Include="MailTemplates\Razor\PasswordlessSignIn.cshtml" />
<EmbeddedResource Include="MailTemplates\Razor\VerifyDelete.cshtml" />

View File

@ -0,0 +1,5 @@
This is a reminder that your Bitwarden subscription is due for renewal soon. Your payment method on file will be charged for **{{amountDue}}** on **{{dueDate}}**.
To avoid any interruption in service, please ensure that your payment method on file is up to date and can be charged for the above amount. You can manage your subscription and payment method by logging into the web vault at <{{vaultUrl}}>. Once logged in, navigate to the Billing page for your account.
If you have any questions or problems, please feel free to email us at hello@bitwarden.com.

View File

@ -0,0 +1,35 @@
@model Bit.Core.Models.Mail.InvoiceUpcomingViewModel
@{
Layout = "_BasicMailLayout";
}
<p>
This is a reminder that your Bitwarden subscription is due for renewal soon.
Your payment method on file will be charged for <b>@Model.AmountDue.ToString("C")</b> on
<b>@Model.DueDate.ToString("MMM dd, yyyy")</b>.
</p>
<p>
<b><u>Summary Of Charges</u></b><br />
@foreach(var item in Model.Items)
{
@:- @item<br />
}
</p>
<p>
To avoid any interruption in service, please ensure that your payment method
on file is up to date and can be charged for the above amount. You can manage your
subscription and payment method by logging into the web vault at
<a href="@Model.WebVaultUrl" target="_blank">@Model.WebVaultUrl</a>. Once logged in,
navigate to the Billing page for your account.
</p>
@if(Model.MentionInvoices)
{
<p>
Invoices for your payments can also be downloaded from Billing page for your account.
</p>
}
<p>
If you have any questions or problems, please feel free to email us at
hello@bitwarden.com.
</p>

View File

@ -0,0 +1,27 @@
@model Bit.Core.Models.Mail.InvoiceUpcomingViewModel
@{
Layout = "_BasicMailLayout.text";
}
This is a reminder that your Bitwarden subscription is due for renewal soon.
Your payment method on file will be charged for @Model.AmountDue.ToString("C") on @Model.DueDate.ToString("MMM dd, yyyy").
Summary Of Charges
------------------
@foreach(var item in Model.Items)
{
@:- @item
}
To avoid any interruption in service, please ensure that your payment method
on file is up to date and can be charged for the above amount. You can manage your
subscription and payment method by logging into the web vault at <@Model.WebVaultUrl>.
Once logged in, navigate to the Billing page for your account.
@if(Model.MentionInvoices)
{
@:
@: Invoices for your payments can also be downloaded from Billing page for your
@: account.
}
If you have any questions or problems, please feel free to email us at
hello@bitwarden.com.

View File

@ -1,18 +0,0 @@
using System;
using Bit.Core.Models.Table;
namespace Bit.Core.Models.Api
{
public class AuthTokenResponseModel : ResponseModel
{
public AuthTokenResponseModel(string token, User user = null)
: base("authToken")
{
Token = token;
Profile = user == null ? null : new ProfileResponseModel(user, null);
}
public string Token { get; set; }
public ProfileResponseModel Profile { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
namespace Bit.Core.Models.Mail
{
public class InvoiceUpcomingViewModel : BaseMailModel
{
public decimal AmountDue { get; set; }
public DateTime DueDate { get; set; }
public List<string> Items { get; set; }
public bool MentionInvoices { get; set; }
}
}

View File

@ -6,6 +6,7 @@ namespace Bit.Core.Models.Mail
{
public string Subject { get; set; }
public IEnumerable<string> ToEmails { get; set; }
public IEnumerable<string> BccEmails { get; set; }
public string HtmlContent { get; set; }
public string TextContent { get; set; }
public IDictionary<string, object> MetaData { get; set; }

View File

@ -19,5 +19,6 @@ namespace Bit.Core.Services
Task SendOrganizationAcceptedEmailAsync(string organizationName, string userEmail, IEnumerable<string> adminEmails);
Task SendOrganizationConfirmedEmailAsync(string organizationName, string email);
Task SendPasswordlessSignInAsync(string returnUrl, string token, string email);
Task SendInvoiceUpcomingAsync(string email, decimal amount, DateTime dueDate, List<string> items, bool mentionInvoices);
}
}

View File

@ -179,6 +179,20 @@ namespace Bit.Core.Services
}
}
public async Task SendInvoiceUpcomingAsync(string email, decimal amount, DateTime dueDate,
List<string> items, bool mentionInvoices)
{
try
{
await _primaryMailService.SendInvoiceUpcomingAsync(email, amount, dueDate, items, mentionInvoices);
}
catch(Exception e)
{
LogError(e);
await _backupMailService.SendInvoiceUpcomingAsync(email, amount, dueDate, items, mentionInvoices);
}
}
private void LogError(Exception e)
{
_logger.LogError(e, "Error sending mail with primary service, using backup.");

View File

@ -190,6 +190,22 @@ namespace Bit.Core.Services
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendInvoiceUpcomingAsync(string email, decimal amount, DateTime dueDate,
List<string> items, bool mentionInvoices)
{
var model = new Dictionary<string, string>
{
["vaultUrl"] = _globalSettings.BaseServiceUri.VaultWithHash,
["dueDate"] = dueDate.ToString("MMM dd, yyyy"),
["amountDue"] = amount.ToString("C")
};
var message = await CreateMessageAsync("Your Subscription Will Renew Soon", email,
"InvoiceUpcoming", model);
message.BccEmails = new List<string> { "kyle@bitwarden.com" };
await _mailDeliveryService.SendEmailAsync(message);
}
private async Task<MailMessage> CreateMessageAsync(string subject, string toEmail, string fileName,
Dictionary<string, string> model)
{

View File

@ -223,6 +223,26 @@ namespace Bit.Core.Services
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendInvoiceUpcomingAsync(string email, decimal amount, DateTime dueDate,
List<string> items, bool mentionInvoices)
{
var message = CreateDefaultMessage("Your Subscription Will Renew Soon", email);
message.BccEmails = new List<string> { "kyle@bitwarden.com" };
var model = new InvoiceUpcomingViewModel
{
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
AmountDue = amount,
DueDate = dueDate,
Items = items,
MentionInvoices = mentionInvoices
};
message.HtmlContent = await _engine.CompileRenderAsync("InvoiceUpcoming", model);
message.TextContent = await _engine.CompileRenderAsync("InvoiceUpcoming.text", model);
await _mailDeliveryService.SendEmailAsync(message);
}
private MailMessage CreateDefaultMessage(string subject, string toEmail)
{
return CreateDefaultMessage(subject, new List<string> { toEmail });

View File

@ -39,6 +39,10 @@ namespace Bit.Core.Services
sendGridMessage.SetClickTracking(true, false);
sendGridMessage.SetOpenTracking(true, null);
sendGridMessage.AddTos(message.ToEmails.Select(e => new EmailAddress(e)).ToList());
if(message.BccEmails?.Any() ?? false)
{
sendGridMessage.AddBccs(message.BccEmails.Select(e => new EmailAddress(e)).ToList());
}
if(message.MetaData?.ContainsKey("SendGridTemplateId") ?? false)
{

View File

@ -50,6 +50,14 @@ namespace Bit.Core.Services
smtpMessage.To.Add(new MailAddress(address));
}
if(message.BccEmails != null)
{
foreach(var address in message.BccEmails)
{
smtpMessage.Bcc.Add(new MailAddress(address));
}
}
if(string.IsNullOrWhiteSpace(message.TextContent))
{
smtpMessage.IsBodyHtml = true;

View File

@ -66,5 +66,11 @@ namespace Bit.Core.Services
{
return Task.FromResult(0);
}
public Task SendInvoiceUpcomingAsync(string email, decimal amount, DateTime dueDate,
List<string> items, bool mentionInvoices)
{
return Task.FromResult(0);
}
}
}