Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[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 intomain
base:main
Choose a base branch
Loading
frombilling/PM-28100/2019-families-email
Open
Show file tree
Hide file tree
Changes from5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some comments aren't visible on the classic Files Changed page.

6 changes: 6 additions & 0 deletionssrc/Billing/Services/IStripeFacade.cs
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}
8 changes: 8 additions & 0 deletionssrc/Billing/Services/Implementations/StripeFacade.cs
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ public class StripeFacade : IStripeFacade
private readonly DiscountService _discountService = new();
private readonly SetupIntentService _setupIntentService = new();
private readonly TestClockService _testClockService = new();
private readonly CouponService _couponService = new();

public async Task<Charge> GetCharge(
string chargeId,
Expand DownExpand Up@@ -143,4 +144,11 @@ public Task<TestClock> GetTestClock(
RequestOptions requestOptions = null,
CancellationToken cancellationToken = default) =>
_testClockService.GetAsync(testClockId, testClockGetOptions, requestOptions, cancellationToken);

public Task<Coupon> GetCoupon(
string couponId,
CouponGetOptions couponGetOptions = null,
RequestOptions requestOptions = null,
CancellationToken cancellationToken = default) =>
_couponService.GetAsync(couponId, couponGetOptions, requestOptions, cancellationToken);
}
152 changes: 125 additions & 27 deletionssrc/Billing/Services/Implementations/UpcomingInvoiceHandler.cs
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
using Bit.Core;
using System.Globalization;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories;
Expand All@@ -8,14 +9,16 @@
using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Billing.Pricing;
using Bit.Core.Entities;
using Bit.Core.Models.Mail.UpdatedInvoiceIncoming;
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;

Expand DownExpand Up@@ -107,13 +110,22 @@ private async Task HandleOrganizationUpcomingInvoiceAsync(

var milestone3 = featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3);

await AlignOrganizationSubscriptionConcernsAsync(
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)
{
Expand All@@ -135,9 +147,7 @@ await AlignOrganizationSubscriptionConcernsAsync(
}
}

await (milestone3
? SendUpdatedUpcomingInvoiceEmailsAsync([organization.BillingEmail])
: SendUpcomingInvoiceEmailsAsync([organization.BillingEmail], invoice));
await SendUpcomingInvoiceEmailsAsync([organization.BillingEmail], invoice);
}

private async Task AlignOrganizationTaxConcernsAsync(
Expand DownExpand Up@@ -188,7 +198,16 @@ await stripeFacade.UpdateSubscription(subscription.Id,
}
}

private async Task AlignOrganizationSubscriptionConcernsAsync(
/// <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,
Expand All@@ -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;
return false;
}

var passwordManagerItem =
Expand All@@ -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;
return false;
}

varfamilies = await pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually);
varfamiliesPlan = await pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually);

organization.PlanType =families.Type;
organization.Plan =families.Name;
organization.UsersGetPremium =families.UsersGetPremium;
organization.Seats =families.PasswordManager.BaseSeats;
organization.PlanType =familiesPlan.Type;
organization.Plan =familiesPlan.Name;
organization.UsersGetPremium =familiesPlan.UsersGetPremium;
organization.Seats =familiesPlan.PasswordManager.BaseSeats;

var options = new SubscriptionUpdateOptions
{
Expand All@@ -225,7 +244,7 @@ private async Task AlignOrganizationSubscriptionConcernsAsync(
new SubscriptionItemOptions
{
Id = passwordManagerItem.Id,
Price =families.PasswordManager.StripePlanId
Price =familiesPlan.PasswordManager.StripePlanId
}
],
ProrationBehavior = ProrationBehavior.None
Expand DownExpand Up@@ -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)
{
Expand All@@ -275,6 +296,7 @@ private async Task AlignOrganizationSubscriptionConcernsAsync(
organization.Id,
@event.Type,
@event.Id);
return false;
}
}

Expand DownExpand Up@@ -303,14 +325,21 @@ private async Task HandlePremiumUsersUpcomingInvoiceAsync(
var milestone2Feature = featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2);
if (milestone2Feature)
{
await AlignPremiumUsersSubscriptionConcernsAsync(user, @event, subscription);
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 (milestone2Feature
? SendUpdatedUpcomingInvoiceEmailsAsync(new List<string> { user.Email })
: SendUpcomingInvoiceEmailsAsync(new List<string> { user.Email }, invoice));
await SendUpcomingInvoiceEmailsAsync(new List<string> { user.Email }, invoice);
}
}

Expand DownExpand Up@@ -341,7 +370,7 @@ await stripeFacade.UpdateSubscription(subscription.Id,
}
}

private async Task AlignPremiumUsersSubscriptionConcernsAsync(
private async Task<bool> AlignPremiumUsersSubscriptionConcernsAsync(
User user,
Event @event,
Subscription subscription)
Expand All@@ -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;
return false;
}

try
Expand All@@ -371,6 +400,8 @@ await stripeFacade.UpdateSubscription(subscription.Id,
],
ProrationBehavior = ProrationBehavior.None
});
await SendPremiumRenewalEmailAsync(user, plan);
return true;
}
catch (Exception exception)
{
Expand All@@ -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;
}
}

Expand DownExpand Up@@ -513,15 +545,81 @@ await mailService.SendInvoiceUpcoming(
}
}

private async Task SendUpdatedUpcomingInvoiceEmailsAsync(IEnumerable<string> emails)
private async Task SendFamiliesRenewalEmailAsync(
Organization organization,
Plan familiesPlan,
Plan planBeforeAlignment)
{
var validEmails = emails.Where(e => !string.IsNullOrEmpty(e));
var updatedUpcomingEmail = new UpdatedInvoiceUpcomingMail
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);
if (coupon == null)
{
logger.LogWarning("Could not find coupon for sending families 2019 email with ID: {CouponID}", CouponIDs.Milestone3SubscriptionDiscount);
return;
}

if (coupon.PercentOff == null)
{
ToEmails = validEmails,
View = new UpdatedInvoiceUpcomingView()
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(updatedUpcomingEmail);

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
Expand Down
1 change: 1 addition & 0 deletionssrc/Core/MailTemplates/Mjml/.mjmlconfig
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
{
"packages": [
"components/mj-bw-hero",
"components/mj-bw-simple-hero",
"components/mj-bw-icon-row",
"components/mj-bw-learn-more-footer",
"emails/AdminConsole/components/mj-bw-inviter-info"
Expand Down
40 changes: 40 additions & 0 deletionssrc/Core/MailTemplates/Mjml/components/mj-bw-simple-hero.js
View file
Open in desktop
Original file line numberDiff line numberDiff 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;
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp