diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index b9667e62a3..9d4b9e6561 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -5,6 +5,7 @@ netcoreapp2.0 Bit.Billing bitwarden-Billing + false diff --git a/src/Billing/Controllers/StripeController.cs b/src/Billing/Controllers/StripeController.cs index 807aef0d36..b30357cb12 100644 --- a/src/Billing/Controllers/StripeController.cs +++ b/src/Billing/Controllers/StripeController.cs @@ -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, 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.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(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; + } + } } } diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index ad08ad5b6a..bfb55a8465 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -7,7 +7,10 @@ + + + @@ -18,6 +21,7 @@ + @@ -26,6 +30,8 @@ + + diff --git a/src/Core/MailTemplates/Markdown/InvoiceUpcoming.md b/src/Core/MailTemplates/Markdown/InvoiceUpcoming.md new file mode 100644 index 0000000000..aa9e671ea5 --- /dev/null +++ b/src/Core/MailTemplates/Markdown/InvoiceUpcoming.md @@ -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. diff --git a/src/Core/MailTemplates/Razor/InvoiceUpcoming.cshtml b/src/Core/MailTemplates/Razor/InvoiceUpcoming.cshtml new file mode 100644 index 0000000000..7872553dea --- /dev/null +++ b/src/Core/MailTemplates/Razor/InvoiceUpcoming.cshtml @@ -0,0 +1,35 @@ +@model Bit.Core.Models.Mail.InvoiceUpcomingViewModel +@{ + Layout = "_BasicMailLayout"; +} +

+ 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. +

diff --git a/src/Core/MailTemplates/Razor/InvoiceUpcoming.text.cshtml b/src/Core/MailTemplates/Razor/InvoiceUpcoming.text.cshtml new file mode 100644 index 0000000000..e092f66c11 --- /dev/null +++ b/src/Core/MailTemplates/Razor/InvoiceUpcoming.text.cshtml @@ -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. diff --git a/src/Core/Models/Api/Response/AuthTokenResponseModel.cs b/src/Core/Models/Api/Response/AuthTokenResponseModel.cs deleted file mode 100644 index b94b7d0e1e..0000000000 --- a/src/Core/Models/Api/Response/AuthTokenResponseModel.cs +++ /dev/null @@ -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; } - } -} diff --git a/src/Core/Models/Mail/InvoiceUpcomingViewModel.cs b/src/Core/Models/Mail/InvoiceUpcomingViewModel.cs new file mode 100644 index 0000000000..1a12760bf8 --- /dev/null +++ b/src/Core/Models/Mail/InvoiceUpcomingViewModel.cs @@ -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 Items { get; set; } + public bool MentionInvoices { get; set; } + } +} diff --git a/src/Core/Models/Mail/MailMessage.cs b/src/Core/Models/Mail/MailMessage.cs index 82e4882543..d94cd04acf 100644 --- a/src/Core/Models/Mail/MailMessage.cs +++ b/src/Core/Models/Mail/MailMessage.cs @@ -6,6 +6,7 @@ namespace Bit.Core.Models.Mail { public string Subject { get; set; } public IEnumerable ToEmails { get; set; } + public IEnumerable BccEmails { get; set; } public string HtmlContent { get; set; } public string TextContent { get; set; } public IDictionary MetaData { get; set; } diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index c2086082fd..9a2a2aefeb 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -19,5 +19,6 @@ namespace Bit.Core.Services Task SendOrganizationAcceptedEmailAsync(string organizationName, string userEmail, IEnumerable adminEmails); Task SendOrganizationConfirmedEmailAsync(string organizationName, string email); Task SendPasswordlessSignInAsync(string returnUrl, string token, string email); + Task SendInvoiceUpcomingAsync(string email, decimal amount, DateTime dueDate, List items, bool mentionInvoices); } } diff --git a/src/Core/Services/Implementations/BackupMailService.cs b/src/Core/Services/Implementations/BackupMailService.cs index b4359c44a3..b5c86ab090 100644 --- a/src/Core/Services/Implementations/BackupMailService.cs +++ b/src/Core/Services/Implementations/BackupMailService.cs @@ -179,6 +179,20 @@ namespace Bit.Core.Services } } + public async Task SendInvoiceUpcomingAsync(string email, decimal amount, DateTime dueDate, + List 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."); diff --git a/src/Core/Services/Implementations/MarkdownMailService.cs b/src/Core/Services/Implementations/MarkdownMailService.cs index e8db1a7dc3..7ab98111ee 100644 --- a/src/Core/Services/Implementations/MarkdownMailService.cs +++ b/src/Core/Services/Implementations/MarkdownMailService.cs @@ -190,6 +190,22 @@ namespace Bit.Core.Services await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendInvoiceUpcomingAsync(string email, decimal amount, DateTime dueDate, + List items, bool mentionInvoices) + { + var model = new Dictionary + { + ["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 { "kyle@bitwarden.com" }; + await _mailDeliveryService.SendEmailAsync(message); + } + private async Task CreateMessageAsync(string subject, string toEmail, string fileName, Dictionary model) { diff --git a/src/Core/Services/Implementations/RazorMailService.cs b/src/Core/Services/Implementations/RazorMailService.cs index 2cb198c141..2950720e0c 100644 --- a/src/Core/Services/Implementations/RazorMailService.cs +++ b/src/Core/Services/Implementations/RazorMailService.cs @@ -223,6 +223,26 @@ namespace Bit.Core.Services await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendInvoiceUpcomingAsync(string email, decimal amount, DateTime dueDate, + List items, bool mentionInvoices) + { + var message = CreateDefaultMessage("Your Subscription Will Renew Soon", email); + message.BccEmails = new List { "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 { toEmail }); diff --git a/src/Core/Services/Implementations/SendGridMailDeliveryService.cs b/src/Core/Services/Implementations/SendGridMailDeliveryService.cs index 9dc077ee58..c1f446c318 100644 --- a/src/Core/Services/Implementations/SendGridMailDeliveryService.cs +++ b/src/Core/Services/Implementations/SendGridMailDeliveryService.cs @@ -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) { diff --git a/src/Core/Services/Implementations/SmtpMailDeliveryService.cs b/src/Core/Services/Implementations/SmtpMailDeliveryService.cs index 135a6e2058..d03505bc19 100644 --- a/src/Core/Services/Implementations/SmtpMailDeliveryService.cs +++ b/src/Core/Services/Implementations/SmtpMailDeliveryService.cs @@ -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; diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index eba42fcea8..fa6ec07ddf 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -66,5 +66,11 @@ namespace Bit.Core.Services { return Task.FromResult(0); } + + public Task SendInvoiceUpcomingAsync(string email, decimal amount, DateTime dueDate, + List items, bool mentionInvoices) + { + return Task.FromResult(0); + } } }