Skip to content

Commit 75ef19d

Browse files
committed
[PM-28100] families 2019 email
1 parent a386350 commit 75ef19d

File tree

8 files changed

+756
-10
lines changed

8 files changed

+756
-10
lines changed

src/Billing/Services/IStripeFacade.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,10 @@ Task<TestClock> GetTestClock(
116116
TestClockGetOptions testClockGetOptions = null,
117117
RequestOptions requestOptions = null,
118118
CancellationToken cancellationToken = default);
119+
120+
Task<Coupon> GetCoupon(
121+
string couponId,
122+
CouponGetOptions couponGetOptions = null,
123+
RequestOptions requestOptions = null,
124+
CancellationToken cancellationToken = default);
119125
}

src/Billing/Services/Implementations/StripeFacade.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class StripeFacade : IStripeFacade
1818
private readonly DiscountService _discountService = new();
1919
private readonly SetupIntentService _setupIntentService = new();
2020
private readonly TestClockService _testClockService = new();
21+
private readonly CouponService _couponService = new();
2122

2223
public async Task<Charge> GetCharge(
2324
string chargeId,
@@ -143,4 +144,11 @@ public Task<TestClock> GetTestClock(
143144
RequestOptions requestOptions = null,
144145
CancellationToken cancellationToken = default) =>
145146
_testClockService.GetAsync(testClockId, testClockGetOptions, requestOptions, cancellationToken);
147+
148+
public Task<Coupon> GetCoupon(
149+
string couponId,
150+
CouponGetOptions couponGetOptions = null,
151+
RequestOptions requestOptions = null,
152+
CancellationToken cancellationToken = default) =>
153+
_couponService.GetAsync(couponId, couponGetOptions, requestOptions, cancellationToken);
146154
}

src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Bit.Core.Billing.Payment.Queries;
1010
using Bit.Core.Billing.Pricing;
1111
using Bit.Core.Entities;
12+
using Bit.Core.Models.Mail.Billing.Renewal.Families2019Renewal;
1213
using Bit.Core.Models.Mail.Billing.Renewal.Families2020Renewal;
1314
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
1415
using Bit.Core.Platform.Mail.Mailer;
@@ -284,7 +285,7 @@ private async Task<bool> AlignOrganizationSubscriptionConcernsAsync(
284285
{
285286
await organizationRepository.ReplaceAsync(organization);
286287
await stripeFacade.UpdateSubscription(subscription.Id, options);
287-
await SendFamiliesRenewalEmailAsync(organization, familiesPlan);
288+
await SendFamiliesRenewalEmailAsync(organization, familiesPlan, plan);
288289
return true;
289290
}
290291
catch (Exception exception)
@@ -546,7 +547,18 @@ await mailService.SendInvoiceUpcoming(
546547

547548
private async Task SendFamiliesRenewalEmailAsync(
548549
Organization organization,
549-
Plan familiesPlan)
550+
Plan familiesPlan,
551+
Plan planBeforeAlignment)
552+
{
553+
await (planBeforeAlignment switch
554+
{
555+
{ Type: PlanType.FamiliesAnnually2025 } => SendFamilies2020RenewalEmailAsync(organization, familiesPlan),
556+
{ Type: PlanType.FamiliesAnnually2019 } => SendFamilies2019RenewalEmailAsync(organization, familiesPlan),
557+
_ => throw new InvalidOperationException("Unsupported families plan in SendFamiliesRenewalEmailAsync().")
558+
});
559+
}
560+
561+
private async Task SendFamilies2020RenewalEmailAsync(Organization organization, Plan familiesPlan)
550562
{
551563
var email = new Families2020RenewalMail
552564
{
@@ -560,6 +572,38 @@ private async Task SendFamiliesRenewalEmailAsync(
560572
await mailer.SendEmail(email);
561573
}
562574

575+
private async Task SendFamilies2019RenewalEmailAsync(Organization organization, Plan familiesPlan)
576+
{
577+
var coupon = await stripeFacade.GetCoupon(CouponIDs.Milestone3SubscriptionDiscount);
578+
if (coupon == null)
579+
{
580+
logger.LogWarning("Could not find coupon for sending families 2019 email with ID: {CouponID}", CouponIDs.Milestone3SubscriptionDiscount);
581+
return;
582+
}
583+
584+
if (coupon.PercentOff == null)
585+
{
586+
logger.LogWarning("The coupon for sending families 2019 email with ID: {CouponID} has a null PercentOff.", CouponIDs.Milestone3SubscriptionDiscount);
587+
return;
588+
}
589+
590+
var discountedAnnualRenewalPrice = familiesPlan.PasswordManager.BasePrice * (100 - coupon.PercentOff.Value) / 100;
591+
592+
var email = new Families2019RenewalMail
593+
{
594+
ToEmails = [organization.BillingEmail],
595+
View = new Families2019RenewalMailView
596+
{
597+
BaseMonthlyRenewalPrice = (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US")),
598+
BaseAnnualRenewalPrice = familiesPlan.PasswordManager.BasePrice.ToString("C", new CultureInfo("en-US")),
599+
DiscountAmount = $"{coupon.PercentOff}%",
600+
DiscountedAnnualRenewalPrice = discountedAnnualRenewalPrice.ToString("C", new CultureInfo("en-US"))
601+
}
602+
};
603+
604+
await mailer.SendEmail(email);
605+
}
606+
563607
private async Task SendPremiumRenewalEmailAsync(
564608
User user,
565609
PremiumPlan premiumPlan)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<mjml>
2+
<mj-head>
3+
<mj-include path="../../../components/head.mjml"/>
4+
</mj-head>
5+
6+
<!-- Blue Header Section-->
7+
<mj-body css-class="border-fix">
8+
<mj-wrapper css-class="border-fix" padding="20px 20px 0px 20px">
9+
<mj-bw-simple-hero />
10+
</mj-wrapper>
11+
12+
<!-- Main Content Section -->
13+
<mj-wrapper padding="0px 20px 0px 20px">
14+
<mj-section background-color="#fff" padding="15px 10px 10px 10px">
15+
<mj-column>
16+
<mj-text font-size="16px" line-height="24px" padding="10px 15px 15px 15px">
17+
Your Bitwarden Families subscription renews in 15 days. The price is updating to {{BaseMonthlyRenewalPrice}}/month, billed annually
18+
at {{BaseAnnualRenewalPrice}} + tax.
19+
</mj-text>
20+
<mj-text font-size="16px" line-height="24px" padding="10px 15px 15px 15px">
21+
As a long time Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this renewal.
22+
This renewal will now be billed annually at {{DiscountedAnnualRenewalPrice}} + tax.
23+
</mj-text>
24+
<mj-text font-size="16px" line-height="24px" padding="10px 15px">
25+
Questions? Contact
26+
<a href="mailto:[email protected]" class="link">[email protected]</a>
27+
</mj-text>
28+
</mj-column>
29+
</mj-section>
30+
<mj-section background-color="#fff" padding="0 20px 20px 20px">
31+
</mj-section>
32+
</mj-wrapper>
33+
34+
<!-- Learn More Section -->
35+
<mj-wrapper padding="0px 20px 10px 20px">
36+
<mj-bw-learn-more-footer/>
37+
</mj-wrapper>
38+
39+
<!-- Footer -->
40+
<mj-include path="../../../components/footer.mjml"/>
41+
</mj-body>
42+
</mjml>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Bit.Core.Platform.Mail.Mailer;
2+
3+
namespace Bit.Core.Models.Mail.Billing.Renewal.Families2019Renewal;
4+
5+
public class Families2019RenewalMailView : BaseMailView
6+
{
7+
public required string BaseMonthlyRenewalPrice { get; set; }
8+
public required string BaseAnnualRenewalPrice { get; set; }
9+
public required string DiscountedAnnualRenewalPrice { get; set; }
10+
public required string DiscountAmount { get; set; }
11+
}
12+
13+
public class Families2019RenewalMail : BaseMail<Families2019RenewalMailView>
14+
{
15+
public override string Subject { get => "Your Bitwarden Families renewal is updating"; }
16+
}

0 commit comments

Comments
 (0)