- Notifications
You must be signed in to change notification settings - Fork1.5k
[PM-28100] families 2019 email#6645
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Open
kdenney wants to merge7 commits intomainChoose a base branch frombilling/PM-28100/2019-families-email
base:main
Could not load branches
Branch not found:{{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline, and old review comments may become outdated.
Uh oh!
There was an error while loading.Please reload this page.
Open
Changes from5 commits
Commits
Show all changes
7 commits Select commitHold shift + click to select a range
244b871 Send F2020 renewal email
amorask-bitwarden7f97444 Merge branch 'main' into billing/PM-26461/families-2020-renewal-email
amorask-bitwardenf999907 Implement and use simple hero
amorask-bitwardena386350 Cy's feedback
amorask-bitwarden75ef19d [PM-28100] families 2019 email
kdenney3956e81 Merge branch 'main' into billing/PM-28100/2019-families-email
kdenneyb6bded9 pr feedback
kdenneyFile filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -116,4 +116,10 @@ Task<TestClock> GetTestClock( | ||
| TestClockGetOptions testClockGetOptions = null, | ||
| RequestOptions requestOptions = null, | ||
| CancellationToken cancellationToken = default); | ||
| Task<Coupon> GetCoupon( | ||
| string couponId, | ||
| CouponGetOptions couponGetOptions = null, | ||
| RequestOptions requestOptions = null, | ||
| CancellationToken cancellationToken = default); | ||
kdenney marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
152 changes: 125 additions & 27 deletionssrc/Billing/Services/Implementations/UpcomingInvoiceHandler.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| using System.Globalization; | ||
| using Bit.Core; | ||
| using Bit.Core.AdminConsole.Entities; | ||
| using Bit.Core.AdminConsole.Entities.Provider; | ||
| using Bit.Core.AdminConsole.Repositories; | ||
| @@ -8,14 +9,16 @@ | ||
| using Bit.Core.Billing.Payment.Queries; | ||
| using Bit.Core.Billing.Pricing; | ||
| using Bit.Core.Entities; | ||
| using Bit.Core.Models.Mail.Billing.Renewal.Families2019Renewal; | ||
| using Bit.Core.Models.Mail.Billing.Renewal.Families2020Renewal; | ||
| using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; | ||
| using Bit.Core.Platform.Mail.Mailer; | ||
| using Bit.Core.Repositories; | ||
| using Bit.Core.Services; | ||
| using Stripe; | ||
| using Event = Stripe.Event; | ||
| using Plan = Bit.Core.Models.StaticStore.Plan; | ||
| using PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan; | ||
| namespace Bit.Billing.Services.Implementations; | ||
| @@ -107,13 +110,22 @@ private async Task HandleOrganizationUpcomingInvoiceAsync( | ||
| var milestone3 = featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3); | ||
| var subscriptionAligned =await AlignOrganizationSubscriptionConcernsAsync( | ||
| organization, | ||
| @event, | ||
| subscription, | ||
| plan, | ||
| milestone3); | ||
| /* | ||
| * Subscription alignment sends out a different version of our Upcoming Invoice email, so we don't need to continue | ||
| * with processing. | ||
| */ | ||
| if (subscriptionAligned) | ||
| { | ||
| return; | ||
| } | ||
| // Don't send the upcoming invoice email unless the organization's on an annual plan. | ||
| if (!plan.IsAnnual) | ||
| { | ||
| @@ -135,9 +147,7 @@ await AlignOrganizationSubscriptionConcernsAsync( | ||
| } | ||
| } | ||
| await SendUpcomingInvoiceEmailsAsync([organization.BillingEmail], invoice); | ||
| } | ||
| private async Task AlignOrganizationTaxConcernsAsync( | ||
| @@ -188,7 +198,16 @@ await stripeFacade.UpdateSubscription(subscription.Id, | ||
| } | ||
| } | ||
| /// <summary> | ||
| /// Aligns the organization's subscription details with the specified plan and milestone requirements. | ||
| /// </summary> | ||
| /// <param name="organization">The organization whose subscription is being updated.</param> | ||
| /// <param name="event">The Stripe event associated with this operation.</param> | ||
| /// <param name="subscription">The organization's subscription.</param> | ||
| /// <param name="plan">The organization's current plan.</param> | ||
| /// <param name="milestone3">A flag indicating whether the third milestone is enabled.</param> | ||
| /// <returns>Whether the operation resulted in an updated subscription.</returns> | ||
| private async Task<bool> AlignOrganizationSubscriptionConcernsAsync( | ||
| Organization organization, | ||
| Event @event, | ||
| Subscription subscription, | ||
| @@ -198,7 +217,7 @@ private async Task AlignOrganizationSubscriptionConcernsAsync( | ||
| // currently these are the only plans that need aligned and both require the same flag and share most of the logic | ||
| if (!milestone3 || plan.Type is not (PlanType.FamiliesAnnually2019 or PlanType.FamiliesAnnually2025)) | ||
| { | ||
| return false; | ||
| } | ||
| var passwordManagerItem = | ||
| @@ -208,15 +227,15 @@ private async Task AlignOrganizationSubscriptionConcernsAsync( | ||
| { | ||
| logger.LogWarning("Could not find Organization's ({OrganizationId}) password manager item while processing '{EventType}' event ({EventID})", | ||
| organization.Id, @event.Type, @event.Id); | ||
| return false; | ||
| } | ||
| varfamiliesPlan = await pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually); | ||
| organization.PlanType =familiesPlan.Type; | ||
| organization.Plan =familiesPlan.Name; | ||
| organization.UsersGetPremium =familiesPlan.UsersGetPremium; | ||
| organization.Seats =familiesPlan.PasswordManager.BaseSeats; | ||
| var options = new SubscriptionUpdateOptions | ||
| { | ||
| @@ -225,7 +244,7 @@ private async Task AlignOrganizationSubscriptionConcernsAsync( | ||
| new SubscriptionItemOptions | ||
| { | ||
| Id = passwordManagerItem.Id, | ||
| Price =familiesPlan.PasswordManager.StripePlanId | ||
| } | ||
| ], | ||
| ProrationBehavior = ProrationBehavior.None | ||
| @@ -266,6 +285,8 @@ private async Task AlignOrganizationSubscriptionConcernsAsync( | ||
| { | ||
| await organizationRepository.ReplaceAsync(organization); | ||
| await stripeFacade.UpdateSubscription(subscription.Id, options); | ||
| await SendFamiliesRenewalEmailAsync(organization, familiesPlan, plan); | ||
| return true; | ||
| } | ||
| catch (Exception exception) | ||
| { | ||
| @@ -275,6 +296,7 @@ private async Task AlignOrganizationSubscriptionConcernsAsync( | ||
| organization.Id, | ||
| @event.Type, | ||
| @event.Id); | ||
| return false; | ||
| } | ||
| } | ||
| @@ -303,14 +325,21 @@ private async Task HandlePremiumUsersUpcomingInvoiceAsync( | ||
| var milestone2Feature = featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2); | ||
| if (milestone2Feature) | ||
| { | ||
| var subscriptionAligned = await AlignPremiumUsersSubscriptionConcernsAsync(user, @event, subscription); | ||
| /* | ||
| * Subscription alignment sends out a different version of our Upcoming Invoice email, so we don't need to continue | ||
| * with processing. | ||
| */ | ||
| if (subscriptionAligned) | ||
| { | ||
| return; | ||
| } | ||
| } | ||
| if (user.Premium) | ||
| { | ||
| await SendUpcomingInvoiceEmailsAsync(new List<string> { user.Email }, invoice); | ||
| } | ||
| } | ||
| @@ -341,7 +370,7 @@ await stripeFacade.UpdateSubscription(subscription.Id, | ||
| } | ||
| } | ||
| private async Task<bool> AlignPremiumUsersSubscriptionConcernsAsync( | ||
| User user, | ||
| Event @event, | ||
| Subscription subscription) | ||
| @@ -352,7 +381,7 @@ private async Task AlignPremiumUsersSubscriptionConcernsAsync( | ||
| { | ||
| logger.LogWarning("Could not find User's ({UserID}) premium subscription item while processing '{EventType}' event ({EventID})", | ||
| user.Id, @event.Type, @event.Id); | ||
| return false; | ||
| } | ||
| try | ||
| @@ -371,6 +400,8 @@ await stripeFacade.UpdateSubscription(subscription.Id, | ||
| ], | ||
| ProrationBehavior = ProrationBehavior.None | ||
| }); | ||
| await SendPremiumRenewalEmailAsync(user, plan); | ||
| return true; | ||
| } | ||
| catch (Exception exception) | ||
| { | ||
| @@ -379,6 +410,7 @@ await stripeFacade.UpdateSubscription(subscription.Id, | ||
| "Failed to update user's ({UserID}) subscription price id while processing event with ID {EventID}", | ||
| user.Id, | ||
| @event.Id); | ||
| return false; | ||
| } | ||
| } | ||
| @@ -513,15 +545,81 @@ await mailService.SendInvoiceUpcoming( | ||
| } | ||
| } | ||
| private async Task SendFamiliesRenewalEmailAsync( | ||
| Organization organization, | ||
| Plan familiesPlan, | ||
| Plan planBeforeAlignment) | ||
| { | ||
| await (planBeforeAlignment switch | ||
| { | ||
| { Type: PlanType.FamiliesAnnually2025 } => SendFamilies2020RenewalEmailAsync(organization, familiesPlan), | ||
| { Type: PlanType.FamiliesAnnually2019 } => SendFamilies2019RenewalEmailAsync(organization, familiesPlan), | ||
| _ => throw new InvalidOperationException("Unsupported families plan in SendFamiliesRenewalEmailAsync().") | ||
| }); | ||
| } | ||
| private async Task SendFamilies2020RenewalEmailAsync(Organization organization, Plan familiesPlan) | ||
| { | ||
| var email = new Families2020RenewalMail | ||
| { | ||
| ToEmails = [organization.BillingEmail], | ||
| View = new Families2020RenewalMailView | ||
| { | ||
| MonthlyRenewalPrice = (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US")) | ||
| } | ||
| }; | ||
| await mailer.SendEmail(email); | ||
| } | ||
| private async Task SendFamilies2019RenewalEmailAsync(Organization organization, Plan familiesPlan) | ||
| { | ||
| var coupon = await stripeFacade.GetCoupon(CouponIDs.Milestone3SubscriptionDiscount); | ||
kdenney marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| if (coupon == null) | ||
| { | ||
| logger.LogWarning("Could not find coupon for sending families 2019 email with ID: {CouponID}", CouponIDs.Milestone3SubscriptionDiscount); | ||
| return; | ||
| } | ||
| if (coupon.PercentOff == null) | ||
| { | ||
| logger.LogWarning("The coupon for sending families 2019 email with ID: {CouponID} has a null PercentOff.", CouponIDs.Milestone3SubscriptionDiscount); | ||
| return; | ||
| } | ||
| var discountedAnnualRenewalPrice = familiesPlan.PasswordManager.BasePrice * (100 - coupon.PercentOff.Value) / 100; | ||
| var email = new Families2019RenewalMail | ||
| { | ||
| ToEmails = [organization.BillingEmail], | ||
| View = new Families2019RenewalMailView | ||
| { | ||
| BaseMonthlyRenewalPrice = (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US")), | ||
| BaseAnnualRenewalPrice = familiesPlan.PasswordManager.BasePrice.ToString("C", new CultureInfo("en-US")), | ||
| DiscountAmount = $"{coupon.PercentOff}%", | ||
| DiscountedAnnualRenewalPrice = discountedAnnualRenewalPrice.ToString("C", new CultureInfo("en-US")) | ||
| } | ||
| }; | ||
| await mailer.SendEmail(email); | ||
| } | ||
| private async Task SendPremiumRenewalEmailAsync( | ||
| User user, | ||
| PremiumPlan premiumPlan) | ||
| { | ||
| /* TODO: Replace with proper premium renewal email template once finalized. | ||
| Using Families2020RenewalMail as a temporary stop-gap. */ | ||
| var email = new Families2020RenewalMail | ||
| { | ||
| ToEmails = [user.Email], | ||
| View = new Families2020RenewalMailView | ||
| { | ||
| MonthlyRenewalPrice = (premiumPlan.Seat.Price / 12).ToString("C", new CultureInfo("en-US")) | ||
| } | ||
| }; | ||
| await mailer.SendEmail(email); | ||
| } | ||
| #endregion | ||
1 change: 1 addition & 0 deletionssrc/Core/MailTemplates/Mjml/.mjmlconfig
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletionssrc/Core/MailTemplates/Mjml/components/mj-bw-simple-hero.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| const { BodyComponent } = require("mjml-core"); | ||
| class MjBwSimpleHero extends BodyComponent { | ||
| static dependencies = { | ||
| // Tell the validator which tags are allowed as our component's parent | ||
| "mj-column": ["mj-bw-simple-hero"], | ||
| "mj-wrapper": ["mj-bw-simple-hero"], | ||
| // Tell the validator which tags are allowed as our component's children | ||
| "mj-bw-simple-hero": [], | ||
| }; | ||
| static allowedAttributes = {}; | ||
| static defaultAttributes = {}; | ||
| render() { | ||
| return this.renderMJML( | ||
| ` | ||
| <mj-section | ||
| full-width="full-width" | ||
| background-color="#175ddc" | ||
| border-radius="4px 4px 0px 0px" | ||
| padding="20px 20px" | ||
| > | ||
| <mj-column width="100%"> | ||
| <mj-image | ||
| align="left" | ||
| src="https://bitwarden.com/images/logo-horizontal-white.png" | ||
| width="150px" | ||
| height="30px" | ||
| padding="10px 5px" | ||
| ></mj-image> | ||
| </mj-column> | ||
| </mj-section> | ||
| `, | ||
| ); | ||
| } | ||
| } | ||
| module.exports = MjBwSimpleHero; |
Oops, something went wrong.
Uh oh!
There was an error while loading.Please reload this page.
Oops, something went wrong.
Uh oh!
There was an error while loading.Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.