Browse Source
* changes to implement the email * Refactoring and fix the unit testing * refactor the code and remove used method * Fix the failing test * Update the email templates * remove the extra space here * Refactor the descriptions * Fix the wrong subject header * Add the in the hyperlink rather than just Help centerpull/6279/head
10 changed files with 914 additions and 1 deletions
@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
|
||||
using System.Text.RegularExpressions; |
||||
using Stripe; |
||||
|
||||
namespace Bit.Core.Billing.Extensions; |
||||
|
||||
public static class InvoiceExtensions |
||||
{ |
||||
/// <summary> |
||||
/// Formats invoice line items specifically for provider invoices, standardizing product descriptions |
||||
/// and ensuring consistent tax representation. |
||||
/// </summary> |
||||
/// <param name="invoice">The Stripe invoice containing line items</param> |
||||
/// <param name="subscription">The associated subscription (for future extensibility)</param> |
||||
/// <returns>A list of formatted invoice item descriptions</returns> |
||||
public static List<string> FormatForProvider(this Invoice invoice, Subscription subscription) |
||||
{ |
||||
var items = new List<string>(); |
||||
|
||||
// Return empty list if no line items |
||||
if (invoice.Lines == null) |
||||
{ |
||||
return items; |
||||
} |
||||
|
||||
foreach (var line in invoice.Lines.Data ?? new List<InvoiceLineItem>()) |
||||
{ |
||||
// Skip null lines or lines without description |
||||
if (line?.Description == null) |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
var description = line.Description; |
||||
|
||||
// Handle Provider Portal and Business Unit Portal service lines |
||||
if (description.Contains("Provider Portal") || description.Contains("Business Unit")) |
||||
{ |
||||
var priceMatch = Regex.Match(description, @"\(at \$[\d,]+\.?\d* / month\)"); |
||||
var priceInfo = priceMatch.Success ? priceMatch.Value : ""; |
||||
|
||||
var standardizedDescription = $"{line.Quantity} × Manage service provider {priceInfo}"; |
||||
items.Add(standardizedDescription); |
||||
} |
||||
// Handle tax lines |
||||
else if (description.ToLower().Contains("tax")) |
||||
{ |
||||
var priceMatch = Regex.Match(description, @"\(at \$[\d,]+\.?\d* / month\)"); |
||||
var priceInfo = priceMatch.Success ? priceMatch.Value : ""; |
||||
|
||||
// If no price info found in description, calculate from amount |
||||
if (string.IsNullOrEmpty(priceInfo) && line.Quantity > 0) |
||||
{ |
||||
var pricePerItem = (line.Amount / 100m) / line.Quantity; |
||||
priceInfo = $"(at ${pricePerItem:F2} / month)"; |
||||
} |
||||
|
||||
var taxDescription = $"{line.Quantity} × Tax {priceInfo}"; |
||||
items.Add(taxDescription); |
||||
} |
||||
// Handle other line items as-is |
||||
else |
||||
{ |
||||
items.Add(description); |
||||
} |
||||
} |
||||
|
||||
// Add fallback tax from invoice-level tax if present and not already included |
||||
if (invoice.Tax.HasValue && invoice.Tax.Value > 0) |
||||
{ |
||||
var taxAmount = invoice.Tax.Value / 100m; |
||||
items.Add($"1 × Tax (at ${taxAmount:F2} / month)"); |
||||
} |
||||
|
||||
return items; |
||||
} |
||||
} |
||||
@ -0,0 +1,211 @@
@@ -0,0 +1,211 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> |
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml"> |
||||
<head> |
||||
<meta name="viewport" content="width=device-width" /> |
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> |
||||
<title>Bitwarden</title> |
||||
</head> |
||||
|
||||
<body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important; margin: 0;" bgcolor="#f6f6f6"> |
||||
<style type="text/css"> |
||||
body { |
||||
margin: 0; |
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; |
||||
box-sizing: border-box; |
||||
font-size: 16px; |
||||
color: #333; |
||||
line-height: 25px; |
||||
-webkit-font-smoothing: antialiased; |
||||
-webkit-text-size-adjust: none; |
||||
} |
||||
|
||||
body * { |
||||
margin: 0; |
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; |
||||
box-sizing: border-box; |
||||
font-size: 16px; |
||||
color: #333; |
||||
line-height: 25px; |
||||
-webkit-font-smoothing: antialiased; |
||||
-webkit-text-size-adjust: none; |
||||
} |
||||
|
||||
img { |
||||
max-width: 100%; |
||||
border: none; |
||||
} |
||||
|
||||
body { |
||||
-webkit-font-smoothing: antialiased; |
||||
-webkit-text-size-adjust: none; |
||||
width: 100% !important; |
||||
height: 100%; |
||||
line-height: 25px; |
||||
} |
||||
|
||||
body { |
||||
background-color: #f6f6f6; |
||||
} |
||||
|
||||
/* Provider-specific styles */ |
||||
.provider-header { |
||||
background-color: #175DDC; |
||||
height: 84px; |
||||
border-top-left-radius: 4px; |
||||
border-top-right-radius: 4px; |
||||
} |
||||
|
||||
.provider-content { |
||||
border-left: 1px solid #e9e9e9; |
||||
border-right: 1px solid #e9e9e9; |
||||
border-bottom: 1px solid #e9e9e9; |
||||
border-bottom-left-radius: 3px; |
||||
border-bottom-right-radius: 3px; |
||||
} |
||||
|
||||
@media only screen and (max-width: 600px) { |
||||
body { |
||||
padding: 0 !important; |
||||
} |
||||
|
||||
.container { |
||||
padding: 0 !important; |
||||
width: 100% !important; |
||||
} |
||||
|
||||
.container-table { |
||||
padding: 0 !important; |
||||
width: 100% !important; |
||||
} |
||||
|
||||
.content { |
||||
padding: 0 0 10px 0 !important; |
||||
} |
||||
|
||||
.content-wrap { |
||||
padding: 10px !important; |
||||
} |
||||
|
||||
.invoice { |
||||
width: 100% !important; |
||||
} |
||||
|
||||
.main { |
||||
border-right: none !important; |
||||
border-left: none !important; |
||||
border-radius: 0 !important; |
||||
} |
||||
|
||||
.provider-header { |
||||
border-radius: 0 !important; |
||||
} |
||||
|
||||
.provider-content { |
||||
border-left: none !important; |
||||
border-right: none !important; |
||||
border-radius: 0 !important; |
||||
} |
||||
|
||||
.logo { |
||||
padding-top: 10px !important; |
||||
} |
||||
|
||||
.footer { |
||||
margin-top: 10px !important; |
||||
} |
||||
|
||||
.indented { |
||||
padding-left: 10px; |
||||
} |
||||
} |
||||
|
||||
@media only screen and (min-width: 600px) { |
||||
{{! Fix for Apple Mail }} |
||||
.content-table { |
||||
width: 600px !important; |
||||
} |
||||
} |
||||
|
||||
/* Component styling - these are explicitly applied via classes so that they can be |
||||
gradually introduced as we update templates.*/ |
||||
a.inline-link { |
||||
font-weight: bold; |
||||
color: #175DDC; |
||||
text-decoration: none; |
||||
} |
||||
|
||||
br.line-break { |
||||
margin: 0; |
||||
box-sizing: border-box; |
||||
color: #333; |
||||
line-height: 25px; |
||||
-webkit-font-smoothing: antialiased; |
||||
-webkit-text-size-adjust: none; |
||||
} |
||||
|
||||
</style> |
||||
{{! Yahoo center fix }} |
||||
<table width="100%" cellpadding="0" cellspacing="0" bgcolor="#f6f6f6"> |
||||
<tr> |
||||
<td class="container" width="100%" align="center"> |
||||
{{! 600px container }} |
||||
<table cellpadding="0" cellspacing="0" width="100%" class="content-table"> |
||||
<tr> |
||||
<td></td> {{! Left column (center fix) }} |
||||
<td class="content" align="center" valign="top" width="660" style="padding-bottom: 20px;"> |
||||
<!-- Blue Header with Logo --> |
||||
<table class="provider-header" cellpadding="0" cellspacing="0" width="660" bgcolor="#175DDC" style="background-color: #175DDC; width: 660px; height: 84px; opacity: 1; border-top-left-radius: 4px; border-top-right-radius: 4px;"> |
||||
<tr> |
||||
<td valign="top" style="height: 20.53px; width: 417px; padding-left: 32px; padding-top: 32px;"> |
||||
<img src="https://assets.bitwarden.com/email/v1/logo-horizontal-white.png" alt="Bitwarden" style="display: block; opacity: 1; width: auto; height: 28px; max-width: 417px;" /> |
||||
</td> |
||||
</tr> |
||||
</table> |
||||
|
||||
<!-- Main Content Container --> |
||||
<table class="main provider-content" cellpadding="0" cellspacing="0" width="660" style="width: 660px; border-left: 1px solid #e9e9e9; border-right: 1px solid #e9e9e9; border-bottom: 1px solid #e9e9e9; border-bottom-left-radius: 3px; border-bottom-right-radius: 3px;" bgcolor="white"> |
||||
<tr> |
||||
<td class="content-wrap" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 20px; -webkit-text-size-adjust: none;" valign="top"> |
||||
|
||||
{{>@partial-block}} |
||||
|
||||
</td> |
||||
</tr> |
||||
</table> |
||||
<table class="footer" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; width: 100%;"> |
||||
<tr> |
||||
<td class="aligncenter social-icons" align="center" style="margin: 0; padding: 15px 0 0 0;" valign="top"> |
||||
<table cellpadding="0" cellspacing="0" style="margin: 0 auto;"> |
||||
<tr> |
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://x.com/bitwarden" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-x.png" alt="X" width="30" height="30" /></a></td> |
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.reddit.com/r/Bitwarden/" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-reddit.png" alt="Reddit" width="30" height="30" /></a></td> |
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://community.bitwarden.com/" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-discourse.png" alt="CommunityForums" width="30" height="30" /></a></td> |
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/bitwarden" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-github.png" alt="GitHub" width="30" height="30" /></a></td> |
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-youtube.png" alt="Youtube" width="30" height="30" /></a></td> |
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.linkedin.com/company/bitwarden1/" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-linkedin.png" alt="LinkedIn" width="30" height="30" /></a></td> |
||||
<td style="margin: 0; padding: 0 10px;" valign="top"><a href="https://www.facebook.com/bitwarden/" target="_blank"><img src="https://assets.bitwarden.com/email/v1/mail-facebook.png" alt="Facebook" width="30" height="30" /></a></td> |
||||
</tr> |
||||
</table> |
||||
</td> |
||||
</tr> |
||||
<tr> |
||||
<td class="content-block" style="font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 12px; font-weight: 400; color: #666666; line-height: 16px; letter-spacing: 0px; margin: 0; -webkit-font-smoothing: antialiased; padding: 15px 0 0 0; -webkit-text-size-adjust: none; text-align: center;" valign="top"> |
||||
© {{CurrentYear}} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa Barbara, CA, USA |
||||
</td> |
||||
</tr> |
||||
<tr> |
||||
<td class="content-block" style="font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 12px; font-weight: 400; color: #999999; line-height: 16px; letter-spacing: 0px; margin: 0; -webkit-font-smoothing: antialiased; padding: 10px 0 0 0; -webkit-text-size-adjust: none; text-align: center;" valign="top"> |
||||
Always confirm you are on an official Bitwarden domain before logging in:<br/> |
||||
<a href="#" style="color: #175DDC; text-decoration: none; font-weight: 700;">bitwarden.com</a> | <a href="#" style="color: #175DDC; text-decoration: none; font-weight: 700;">Learn why we include this</a> |
||||
</td> |
||||
</tr> |
||||
</table> |
||||
</td> |
||||
<td></td> {{! Right column (center fix) }} |
||||
</tr> |
||||
</table> |
||||
</td> |
||||
</tr> |
||||
</table> |
||||
</body> |
||||
</html> |
||||
@ -0,0 +1,89 @@
@@ -0,0 +1,89 @@
|
||||
{{#>ProviderFull}} |
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #1B2029; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #1B2029; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 20px; -webkit-text-size-adjust: none;" valign="top"> |
||||
{{#if (eq CollectionMethod "send_invoice")}} |
||||
<div style="font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: 600; font-size: 24px; line-height: 32px; letter-spacing: 0px; color: #1B2029; margin: 0 0 8px 0;">Your subscription will renew soon</div> |
||||
<div style="font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: 400; font-size: 16px; line-height: 24px; letter-spacing: 0px; color: #1B2029; margin: 0;">On <strong>{{date DueDate 'MMMM dd, yyyy'}}</strong> we'll send you an invoice with a summary of the charges including tax.</div> |
||||
{{else}} |
||||
<div style="font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: 600; font-size: 24px; line-height: 32px; letter-spacing: 0px; color: #1B2029; margin: 0 0 8px 0;">Your subscription will renew on {{date DueDate 'MMMM dd, yyyy'}}</div> |
||||
{{#if HasPaymentMethod}} |
||||
<div style="font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: 400; font-size: 16px; line-height: 24px; letter-spacing: 0px; color: #1B2029; margin: 0;">To avoid any interruption in service, please ensure your {{PaymentMethodDescription}} can be charged for the following amount:</div> |
||||
{{else}} |
||||
<div style="font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: 400; font-size: 16px; line-height: 24px; letter-spacing: 0px; color: #1B2029; margin: 0;">To avoid any interruption in service, please add a payment method that can be charged for the following amount:</div> |
||||
{{/if}} |
||||
{{/if}} |
||||
</td> |
||||
</tr> |
||||
{{#unless (eq CollectionMethod "send_invoice")}} |
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #1B2029; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 32px; font-weight: bold; color: #1B2029; line-height: 1.2; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 20px; -webkit-text-size-adjust: none;" valign="top"> |
||||
{{usd AmountDue}} |
||||
</td> |
||||
</tr> |
||||
{{/unless}} |
||||
{{#if Items}} |
||||
{{#unless (eq CollectionMethod "send_invoice")}} |
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #1B2029; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
||||
<td class="content-block" style="font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; font-weight: 400; color: #1B2029; line-height: 24px; letter-spacing: 0px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> |
||||
<strong style="margin: 0; font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; font-weight: 700; color: ##1B2029; line-height: 24px; letter-spacing: 0px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Summary Of Charges</strong><br /> |
||||
<div style="border-bottom: 1px solid #ddd; margin: 5px 0 10px 0; padding-bottom: 5px;"></div> |
||||
{{#each Items}} |
||||
<div style="font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: 400; font-size: 16px; line-height: 24px; letter-spacing: 0px; color: #1B2029; margin: 0;">{{this}}</div> |
||||
{{/each}} |
||||
</td> |
||||
</tr> |
||||
{{/unless}} |
||||
{{/if}} |
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 20px; -webkit-text-size-adjust: none;" valign="top"> |
||||
{{#if (eq CollectionMethod "send_invoice")}} |
||||
<div style="font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: 400; font-size: 16px; line-height: 24px; letter-spacing: 0px; color: #1B2029; margin: 0;">To avoid any interruption in service for you or your clients, please pay the invoice by the due date, or contact Bitwarden Customer Support to sign up for auto-pay.</div> |
||||
{{else}} |
||||
|
||||
{{/if}} |
||||
</td> |
||||
</tr> |
||||
{{#unless (eq CollectionMethod "send_invoice")}} |
||||
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> |
||||
<table cellpadding="0" cellspacing="0" style="margin: 0;"> |
||||
<tr> |
||||
<td style="background-color: #175DDC; border-radius: 25px; padding: 12px 24px;"> |
||||
<a href="{{{UpdateBillingInfoUrl}}}" style="color: #ffffff; text-decoration: none; font-weight: 500; font-size: 16px;">Update payment method</a> |
||||
</td> |
||||
</tr> |
||||
</table> |
||||
</td> |
||||
</tr> |
||||
{{/unless}} |
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 16px; -webkit-text-size-adjust: none;" valign="top"> |
||||
{{#if (eq CollectionMethod "send_invoice")}} |
||||
<table cellpadding="0" cellspacing="0" style="margin: 0;"> |
||||
<tr> |
||||
<td style="background-color: #175DDC; border-radius: 25px; padding: 12px 24px;"> |
||||
<a href="{{{ContactUrl}}}" style="color: #ffffff; text-decoration: none; font-weight: 500; font-size: 16px;">Contact Bitwarden Support</a> |
||||
</td> |
||||
</tr> |
||||
</table> |
||||
{{/if}} |
||||
</td> |
||||
</tr> |
||||
{{#if (eq CollectionMethod "send_invoice")}} |
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
||||
<td class="content-block last" style="font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; font-weight: 400; color: #1B2029; line-height: 20px; letter-spacing: 0px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top"> |
||||
For assistance managing your subscription, please visit <a href="https://bitwarden.com/help/update-billing-info" style="color: #175DDC !important; text-decoration: none; font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 700; line-height: 20px; letter-spacing: 0px;"><strong style="color: #175DDC !important;">the Help Center</strong></a> or <a href="https://bitwarden.com/contact/" style="color: #175DDC !important; text-decoration: none; font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 700; line-height: 20px; letter-spacing: 0px;"><strong style="color: #175DDC !important;">contact Bitwarden Customer Support</strong></a>. |
||||
</td> |
||||
</tr> |
||||
{{/if}} |
||||
{{#unless (eq CollectionMethod "send_invoice")}} |
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> |
||||
<td class="content-block last" style="font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; font-weight: 400; color: #1B2029; line-height: 20px; letter-spacing: 0px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top"> |
||||
For assistance managing your subscription, please visit <a href="https://bitwarden.com/help/update-billing-info" style="color: #175DDC !important; text-decoration: none; font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 700; line-height: 20px; letter-spacing: 0px;"><strong style="color: #175DDC !important;">the Help Center</strong></a> or <a href="https://bitwarden.com/contact/" style="color: #175DDC !important; text-decoration: none; font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 700; line-height: 20px; letter-spacing: 0px;"><strong style="color: #175DDC !important;">contact Bitwarden Customer Support</strong></a>. |
||||
</td> |
||||
</tr> |
||||
{{/unless}} |
||||
</table> |
||||
{{/ProviderFull}} |
||||
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
{{#>BasicTextLayout}} |
||||
{{#if (eq CollectionMethod "send_invoice")}} |
||||
Your subscription will renew soon |
||||
|
||||
On {{date DueDate 'MMMM dd, yyyy'}} we'll send you an invoice with a summary of the charges including tax. |
||||
{{else}} |
||||
Your subscription will renew on {{date DueDate 'MMMM dd, yyyy'}} |
||||
|
||||
{{#if HasPaymentMethod}} |
||||
To avoid any interruption in service, please ensure your {{PaymentMethodDescription}} can be charged for the following amount: |
||||
{{else}} |
||||
To avoid any interruption in service, please add a payment method that can be charged for the following amount: |
||||
{{/if}} |
||||
|
||||
{{usd AmountDue}} |
||||
{{/if}} |
||||
{{#if Items}} |
||||
{{#unless (eq CollectionMethod "send_invoice")}} |
||||
|
||||
Summary Of Charges |
||||
------------------ |
||||
{{#each Items}} |
||||
{{this}} |
||||
{{/each}} |
||||
{{/unless}} |
||||
{{/if}} |
||||
|
||||
{{#if (eq CollectionMethod "send_invoice")}} |
||||
To avoid any interruption in service for you or your clients, please pay the invoice by the due date, or contact Bitwarden Customer Support to sign up for auto-pay. |
||||
|
||||
Contact Bitwarden Support: {{{ContactUrl}}} |
||||
|
||||
For assistance managing your subscription, please visit the **Help center** (https://bitwarden.com/help/update-billing-info) or **contact Bitwarden Customer Support** (https://bitwarden.com/contact/). |
||||
{{else}} |
||||
|
||||
{{/if}} |
||||
|
||||
{{#unless (eq CollectionMethod "send_invoice")}} |
||||
For assistance managing your subscription, please visit the **Help center** (https://bitwarden.com/help/update-billing-info) or **contact Bitwarden Customer Support** (https://bitwarden.com/contact/). |
||||
{{/unless}} |
||||
{{/BasicTextLayout}} |
||||
@ -0,0 +1,394 @@
@@ -0,0 +1,394 @@
|
||||
using Bit.Core.Billing.Extensions; |
||||
using Stripe; |
||||
using Xunit; |
||||
|
||||
namespace Bit.Core.Test.Billing.Extensions; |
||||
|
||||
public class InvoiceExtensionsTests |
||||
{ |
||||
private static Invoice CreateInvoiceWithLines(params InvoiceLineItem[] lineItems) |
||||
{ |
||||
return new Invoice |
||||
{ |
||||
Lines = new StripeList<InvoiceLineItem> |
||||
{ |
||||
Data = lineItems?.ToList() ?? new List<InvoiceLineItem>() |
||||
} |
||||
}; |
||||
} |
||||
|
||||
#region FormatForProvider Tests |
||||
|
||||
[Fact] |
||||
public void FormatForProvider_NullLines_ReturnsEmptyList() |
||||
{ |
||||
// Arrange |
||||
var invoice = new Invoice |
||||
{ |
||||
Lines = null |
||||
}; |
||||
var subscription = new Subscription(); |
||||
|
||||
// Act |
||||
var result = invoice.FormatForProvider(subscription); |
||||
|
||||
// Assert |
||||
Assert.NotNull(result); |
||||
Assert.Empty(result); |
||||
} |
||||
|
||||
[Fact] |
||||
public void FormatForProvider_EmptyLines_ReturnsEmptyList() |
||||
{ |
||||
// Arrange |
||||
var invoice = CreateInvoiceWithLines(); |
||||
var subscription = new Subscription(); |
||||
|
||||
// Act |
||||
var result = invoice.FormatForProvider(subscription); |
||||
|
||||
// Assert |
||||
Assert.NotNull(result); |
||||
Assert.Empty(result); |
||||
} |
||||
|
||||
[Fact] |
||||
public void FormatForProvider_NullLineItem_SkipsNullLine() |
||||
{ |
||||
// Arrange |
||||
var invoice = CreateInvoiceWithLines(null); |
||||
var subscription = new Subscription(); |
||||
|
||||
// Act |
||||
var result = invoice.FormatForProvider(subscription); |
||||
|
||||
// Assert |
||||
Assert.NotNull(result); |
||||
Assert.Empty(result); |
||||
} |
||||
|
||||
[Fact] |
||||
public void FormatForProvider_LineWithNullDescription_SkipsLine() |
||||
{ |
||||
// Arrange |
||||
var invoice = CreateInvoiceWithLines( |
||||
new InvoiceLineItem { Description = null, Quantity = 1, Amount = 1000 } |
||||
); |
||||
var subscription = new Subscription(); |
||||
|
||||
// Act |
||||
var result = invoice.FormatForProvider(subscription); |
||||
|
||||
// Assert |
||||
Assert.NotNull(result); |
||||
Assert.Empty(result); |
||||
} |
||||
|
||||
[Fact] |
||||
public void FormatForProvider_ProviderPortalTeams_FormatsCorrectly() |
||||
{ |
||||
// Arrange |
||||
var invoice = CreateInvoiceWithLines( |
||||
new InvoiceLineItem |
||||
{ |
||||
Description = "Provider Portal - Teams (at $6.00 / month)", |
||||
Quantity = 5, |
||||
Amount = 3000 |
||||
} |
||||
); |
||||
var subscription = new Subscription(); |
||||
|
||||
// Act |
||||
var result = invoice.FormatForProvider(subscription); |
||||
|
||||
// Assert |
||||
Assert.Single(result); |
||||
Assert.Equal("5 × Manage service provider (at $6.00 / month)", result[0]); |
||||
} |
||||
|
||||
[Fact] |
||||
public void FormatForProvider_ProviderPortalEnterprise_FormatsCorrectly() |
||||
{ |
||||
// Arrange |
||||
var invoice = CreateInvoiceWithLines( |
||||
new InvoiceLineItem |
||||
{ |
||||
Description = "Provider Portal - Enterprise (at $4.00 / month)", |
||||
Quantity = 10, |
||||
Amount = 4000 |
||||
} |
||||
); |
||||
var subscription = new Subscription(); |
||||
|
||||
// Act |
||||
var result = invoice.FormatForProvider(subscription); |
||||
|
||||
// Assert |
||||
Assert.Single(result); |
||||
Assert.Equal("10 × Manage service provider (at $4.00 / month)", result[0]); |
||||
} |
||||
|
||||
[Fact] |
||||
public void FormatForProvider_ProviderPortalWithoutPriceInfo_FormatsWithoutPrice() |
||||
{ |
||||
// Arrange |
||||
var invoice = CreateInvoiceWithLines( |
||||
new InvoiceLineItem |
||||
{ |
||||
Description = "Provider Portal - Teams", |
||||
Quantity = 3, |
||||
Amount = 1800 |
||||
} |
||||
); |
||||
var subscription = new Subscription(); |
||||
|
||||
// Act |
||||
var result = invoice.FormatForProvider(subscription); |
||||
|
||||
// Assert |
||||
Assert.Single(result); |
||||
Assert.Equal("3 × Manage service provider ", result[0]); |
||||
} |
||||
|
||||
[Fact] |
||||
public void FormatForProvider_BusinessUnitPortalEnterprise_FormatsCorrectly() |
||||
{ |
||||
// Arrange |
||||
var invoice = CreateInvoiceWithLines( |
||||
new InvoiceLineItem |
||||
{ |
||||
Description = "Business Unit Portal - Enterprise (at $5.00 / month)", |
||||
Quantity = 8, |
||||
Amount = 4000 |
||||
} |
||||
); |
||||
var subscription = new Subscription(); |
||||
|
||||
// Act |
||||
var result = invoice.FormatForProvider(subscription); |
||||
|
||||
// Assert |
||||
Assert.Single(result); |
||||
Assert.Equal("8 × Manage service provider (at $5.00 / month)", result[0]); |
||||
} |
||||
|
||||
[Fact] |
||||
public void FormatForProvider_BusinessUnitPortalGeneric_FormatsCorrectly() |
||||
{ |
||||
// Arrange |
||||
var invoice = CreateInvoiceWithLines( |
||||
new InvoiceLineItem |
||||
{ |
||||
Description = "Business Unit Portal (at $3.00 / month)", |
||||
Quantity = 2, |
||||
Amount = 600 |
||||
} |
||||
); |
||||
var subscription = new Subscription(); |
||||
|
||||
// Act |
||||
var result = invoice.FormatForProvider(subscription); |
||||
|
||||
// Assert |
||||
Assert.Single(result); |
||||
Assert.Equal("2 × Manage service provider (at $3.00 / month)", result[0]); |
||||
} |
||||
|
||||
[Fact] |
||||
public void FormatForProvider_TaxLineWithPriceInfo_FormatsCorrectly() |
||||
{ |
||||
// Arrange |
||||
var invoice = CreateInvoiceWithLines( |
||||
new InvoiceLineItem |
||||
{ |
||||
Description = "Tax (at $2.00 / month)", |
||||
Quantity = 1, |
||||
Amount = 200 |
||||
} |
||||
); |
||||
var subscription = new Subscription(); |
||||
|
||||
// Act |
||||
var result = invoice.FormatForProvider(subscription); |
||||
|
||||
// Assert |
||||
Assert.Single(result); |
||||
Assert.Equal("1 × Tax (at $2.00 / month)", result[0]); |
||||
} |
||||
|
||||
[Fact] |
||||
public void FormatForProvider_TaxLineWithoutPriceInfo_CalculatesPrice() |
||||
{ |
||||
// Arrange |
||||
var invoice = CreateInvoiceWithLines( |
||||
new InvoiceLineItem |
||||
{ |
||||
Description = "Tax", |
||||
Quantity = 2, |
||||
Amount = 400 // $4.00 total, $2.00 per item |
||||
} |
||||
); |
||||
var subscription = new Subscription(); |
||||
|
||||
// Act |
||||
var result = invoice.FormatForProvider(subscription); |
||||
|
||||
// Assert |
||||
Assert.Single(result); |
||||
Assert.Equal("2 × Tax (at $2.00 / month)", result[0]); |
||||
} |
||||
|
||||
[Fact] |
||||
public void FormatForProvider_TaxLineWithZeroQuantity_DoesNotCalculatePrice() |
||||
{ |
||||
// Arrange |
||||
var invoice = CreateInvoiceWithLines( |
||||
new InvoiceLineItem |
||||
{ |
||||
Description = "Tax", |
||||
Quantity = 0, |
||||
Amount = 200 |
||||
} |
||||
); |
||||
var subscription = new Subscription(); |
||||
|
||||
// Act |
||||
var result = invoice.FormatForProvider(subscription); |
||||
|
||||
// Assert |
||||
Assert.Single(result); |
||||
Assert.Equal("0 × Tax ", result[0]); |
||||
} |
||||
|
||||
[Fact] |
||||
public void FormatForProvider_OtherLineItem_ReturnsAsIs() |
||||
{ |
||||
// Arrange |
||||
var invoice = CreateInvoiceWithLines( |
||||
new InvoiceLineItem |
||||
{ |
||||
Description = "Some other service", |
||||
Quantity = 1, |
||||
Amount = 1000 |
||||
} |
||||
); |
||||
var subscription = new Subscription(); |
||||
|
||||
// Act |
||||
var result = invoice.FormatForProvider(subscription); |
||||
|
||||
// Assert |
||||
Assert.Single(result); |
||||
Assert.Equal("Some other service", result[0]); |
||||
} |
||||
|
||||
[Fact] |
||||
public void FormatForProvider_InvoiceLevelTax_AddsToResult() |
||||
{ |
||||
// Arrange |
||||
var invoice = CreateInvoiceWithLines( |
||||
new InvoiceLineItem |
||||
{ |
||||
Description = "Provider Portal - Teams", |
||||
Quantity = 1, |
||||
Amount = 600 |
||||
} |
||||
); |
||||
invoice.Tax = 120; // $1.20 in cents |
||||
var subscription = new Subscription(); |
||||
|
||||
// Act |
||||
var result = invoice.FormatForProvider(subscription); |
||||
|
||||
// Assert |
||||
Assert.Equal(2, result.Count); |
||||
Assert.Equal("1 × Manage service provider ", result[0]); |
||||
Assert.Equal("1 × Tax (at $1.20 / month)", result[1]); |
||||
} |
||||
|
||||
[Fact] |
||||
public void FormatForProvider_NoInvoiceLevelTax_DoesNotAddTax() |
||||
{ |
||||
// Arrange |
||||
var invoice = CreateInvoiceWithLines( |
||||
new InvoiceLineItem |
||||
{ |
||||
Description = "Provider Portal - Teams", |
||||
Quantity = 1, |
||||
Amount = 600 |
||||
} |
||||
); |
||||
invoice.Tax = null; |
||||
var subscription = new Subscription(); |
||||
|
||||
// Act |
||||
var result = invoice.FormatForProvider(subscription); |
||||
|
||||
// Assert |
||||
Assert.Single(result); |
||||
Assert.Equal("1 × Manage service provider ", result[0]); |
||||
} |
||||
|
||||
[Fact] |
||||
public void FormatForProvider_ZeroInvoiceLevelTax_DoesNotAddTax() |
||||
{ |
||||
// Arrange |
||||
var invoice = CreateInvoiceWithLines( |
||||
new InvoiceLineItem |
||||
{ |
||||
Description = "Provider Portal - Teams", |
||||
Quantity = 1, |
||||
Amount = 600 |
||||
} |
||||
); |
||||
invoice.Tax = 0; |
||||
var subscription = new Subscription(); |
||||
|
||||
// Act |
||||
var result = invoice.FormatForProvider(subscription); |
||||
|
||||
// Assert |
||||
Assert.Single(result); |
||||
Assert.Equal("1 × Manage service provider ", result[0]); |
||||
} |
||||
|
||||
[Fact] |
||||
public void FormatForProvider_ComplexScenario_HandlesAllLineTypes() |
||||
{ |
||||
// Arrange |
||||
var lineItems = new StripeList<InvoiceLineItem>(); |
||||
lineItems.Data = new List<InvoiceLineItem> |
||||
{ |
||||
new InvoiceLineItem |
||||
{ |
||||
Description = "Provider Portal - Teams (at $6.00 / month)", Quantity = 5, Amount = 3000 |
||||
}, |
||||
new InvoiceLineItem |
||||
{ |
||||
Description = "Provider Portal - Enterprise (at $4.00 / month)", Quantity = 10, Amount = 4000 |
||||
}, |
||||
new InvoiceLineItem { Description = "Tax", Quantity = 1, Amount = 800 }, |
||||
new InvoiceLineItem { Description = "Custom Service", Quantity = 2, Amount = 2000 } |
||||
}; |
||||
|
||||
var invoice = new Invoice |
||||
{ |
||||
Lines = lineItems, |
||||
Tax = 200 // Additional $2.00 tax |
||||
}; |
||||
var subscription = new Subscription(); |
||||
|
||||
// Act |
||||
var result = invoice.FormatForProvider(subscription); |
||||
|
||||
// Assert |
||||
Assert.Equal(5, result.Count); |
||||
Assert.Equal("5 × Manage service provider (at $6.00 / month)", result[0]); |
||||
Assert.Equal("10 × Manage service provider (at $4.00 / month)", result[1]); |
||||
Assert.Equal("1 × Tax (at $8.00 / month)", result[2]); |
||||
Assert.Equal("Custom Service", result[3]); |
||||
Assert.Equal("1 × Tax (at $2.00 / month)", result[4]); |
||||
} |
||||
|
||||
#endregion |
||||
} |
||||
Loading…
Reference in new issue