1
0
mirror of https://github.com/bitwarden/server synced 2025-12-22 11:13:27 +00:00

[PM-24273] Milestone 2C (#6544)

* feat(billing): add mjml template and updated templates

* feat(billing): update maileservices

* feat(billing): add milestone2 discount

* feat(billing): add milestone 2 updates and stripe constants

* tests(billing): add handler tests

* fix(billing): update mailer view and templates

* fix(billing): revert mailservice changes

* fix(billing): swap mailer service in handler

* test(billing): update handler tests
This commit is contained in:
Stephon Brown
2025-11-06 13:21:29 -05:00
committed by GitHub
parent 087c6915e7
commit 5dbce33f74
7 changed files with 1089 additions and 10 deletions

View File

@@ -2,18 +2,22 @@
#nullable disable
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Billing.Pricing;
using Bit.Core.Entities;
using Bit.Core.Models.Mail.UpdatedInvoiceIncoming;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Platform.Mail.Mailer;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Stripe;
using static Bit.Core.Billing.Constants.StripeConstants;
using Event = Stripe.Event;
namespace Bit.Billing.Services.Implementations;
@@ -29,7 +33,9 @@ public class UpcomingInvoiceHandler(
IStripeEventService stripeEventService,
IStripeEventUtilityService stripeEventUtilityService,
IUserRepository userRepository,
IValidateSponsorshipCommand validateSponsorshipCommand)
IValidateSponsorshipCommand validateSponsorshipCommand,
IMailer mailer,
IFeatureService featureService)
: IUpcomingInvoiceHandler
{
public async Task HandleAsync(Event parsedEvent)
@@ -37,7 +43,8 @@ public class UpcomingInvoiceHandler(
var invoice = await stripeEventService.GetInvoice(parsedEvent);
var customer =
await stripeFacade.GetCustomer(invoice.CustomerId, new CustomerGetOptions { Expand = ["subscriptions", "tax", "tax_ids"] });
await stripeFacade.GetCustomer(invoice.CustomerId,
new CustomerGetOptions { Expand = ["subscriptions", "tax", "tax_ids"] });
var subscription = customer.Subscriptions.FirstOrDefault();
@@ -68,7 +75,8 @@ public class UpcomingInvoiceHandler(
if (stripeEventUtilityService.IsSponsoredSubscription(subscription))
{
var sponsorshipIsValid = await validateSponsorshipCommand.ValidateSponsorshipAsync(organizationId.Value);
var sponsorshipIsValid =
await validateSponsorshipCommand.ValidateSponsorshipAsync(organizationId.Value);
if (!sponsorshipIsValid)
{
@@ -122,9 +130,17 @@ public class UpcomingInvoiceHandler(
}
}
var milestone2Feature = featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2);
if (milestone2Feature)
{
await UpdateSubscriptionItemPriceIdAsync(parsedEvent, subscription, user);
}
if (user.Premium)
{
await SendUpcomingInvoiceEmailsAsync(new List<string> { user.Email }, invoice);
await (milestone2Feature
? SendUpdatedUpcomingInvoiceEmailsAsync(new List<string> { user.Email })
: SendUpcomingInvoiceEmailsAsync(new List<string> { user.Email }, invoice));
}
}
else if (providerId.HasValue)
@@ -142,6 +158,39 @@ public class UpcomingInvoiceHandler(
}
}
private async Task UpdateSubscriptionItemPriceIdAsync(Event parsedEvent, Subscription subscription, User user)
{
var pricingItem =
subscription.Items.FirstOrDefault(i => i.Price.Id == Prices.PremiumAnnually);
if (pricingItem != null)
{
try
{
var plan = await pricingClient.GetAvailablePremiumPlan();
await stripeFacade.UpdateSubscription(subscription.Id,
new SubscriptionUpdateOptions
{
Items =
[
new SubscriptionItemOptions { Id = pricingItem.Id, Price = plan.Seat.StripePriceId }
],
Discounts =
[
new SubscriptionDiscountOptions { Coupon = CouponIDs.Milestone2SubscriptionDiscount }
]
});
}
catch (Exception exception)
{
logger.LogError(
exception,
"Failed to update user's ({UserID}) subscription price id while processing event with ID {EventID}",
user.Id,
parsedEvent.Id);
}
}
}
private async Task SendUpcomingInvoiceEmailsAsync(IEnumerable<string> emails, Invoice invoice)
{
var validEmails = emails.Where(e => !string.IsNullOrEmpty(e));
@@ -159,7 +208,19 @@ public class UpcomingInvoiceHandler(
}
}
private async Task SendProviderUpcomingInvoiceEmailsAsync(IEnumerable<string> emails, Invoice invoice, Subscription subscription, Guid providerId)
private async Task SendUpdatedUpcomingInvoiceEmailsAsync(IEnumerable<string> emails)
{
var validEmails = emails.Where(e => !string.IsNullOrEmpty(e));
var updatedUpcomingEmail = new UpdatedInvoiceUpcomingMail
{
ToEmails = validEmails,
View = new UpdatedInvoiceUpcomingView()
};
await mailer.SendEmail(updatedUpcomingEmail);
}
private async Task SendProviderUpcomingInvoiceEmailsAsync(IEnumerable<string> emails, Invoice invoice,
Subscription subscription, Guid providerId)
{
var validEmails = emails.Where(e => !string.IsNullOrEmpty(e));
@@ -205,12 +266,12 @@ public class UpcomingInvoiceHandler(
organization.PlanType.GetProductTier() != ProductTierType.Families &&
customer.Address.Country != Core.Constants.CountryAbbreviations.UnitedStates;
if (nonUSBusinessUse && customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
if (nonUSBusinessUse && customer.TaxExempt != TaxExempt.Reverse)
{
try
{
await stripeFacade.UpdateCustomer(subscription.CustomerId,
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
new CustomerUpdateOptions { TaxExempt = TaxExempt.Reverse });
}
catch (Exception exception)
{
@@ -250,12 +311,12 @@ public class UpcomingInvoiceHandler(
string eventId)
{
if (customer.Address.Country != Core.Constants.CountryAbbreviations.UnitedStates &&
customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
customer.TaxExempt != TaxExempt.Reverse)
{
try
{
await stripeFacade.UpdateCustomer(subscription.CustomerId,
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
new CustomerUpdateOptions { TaxExempt = TaxExempt.Reverse });
}
catch (Exception exception)
{