mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
[PM-28250] Fix seat add on defect (#6580)
* Handle seat add on * Remove old price instead
This commit is contained in:
@@ -220,7 +220,8 @@ public class UpcomingInvoiceHandler(
|
||||
[
|
||||
new SubscriptionItemOptions
|
||||
{
|
||||
Id = passwordManagerItem.Id, Price = families.PasswordManager.StripePlanId
|
||||
Id = passwordManagerItem.Id,
|
||||
Price = families.PasswordManager.StripePlanId
|
||||
}
|
||||
],
|
||||
Discounts =
|
||||
@@ -242,6 +243,17 @@ public class UpcomingInvoiceHandler(
|
||||
});
|
||||
}
|
||||
|
||||
var seatAddOnItem = subscription.Items.FirstOrDefault(item => item.Price.Id == "personal-org-seat-annually");
|
||||
|
||||
if (seatAddOnItem != null)
|
||||
{
|
||||
options.Items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Id = seatAddOnItem.Id,
|
||||
Deleted = true
|
||||
});
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await organizationRepository.ReplaceAsync(organization);
|
||||
|
||||
@@ -1469,4 +1469,324 @@ public class UpcomingInvoiceHandlerTests
|
||||
email.ToEmails.Contains("org@example.com") &&
|
||||
email.Subject == "Your Subscription Will Renew Soon"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WhenMilestone3Enabled_AndSeatAddOnExists_DeletesItem()
|
||||
{
|
||||
// Arrange
|
||||
var parsedEvent = new Event { Id = "evt_123", Type = "invoice.upcoming" };
|
||||
var customerId = "cus_123";
|
||||
var subscriptionId = "sub_123";
|
||||
var passwordManagerItemId = "si_pm_123";
|
||||
var seatAddOnItemId = "si_seat_123";
|
||||
|
||||
var invoice = new Invoice
|
||||
{
|
||||
CustomerId = customerId,
|
||||
AmountDue = 40000,
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
}
|
||||
};
|
||||
|
||||
var families2019Plan = new Families2019Plan();
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = subscriptionId,
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = passwordManagerItemId,
|
||||
Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = seatAddOnItemId,
|
||||
Price = new Price { Id = "personal-org-seat-annually" },
|
||||
Quantity = 3
|
||||
}
|
||||
}
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = _organizationId,
|
||||
BillingEmail = "org@example.com",
|
||||
PlanType = PlanType.FamiliesAnnually2019
|
||||
};
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
_stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
|
||||
_stripeEventUtilityService
|
||||
.GetIdsFromMetadata(subscription.Metadata)
|
||||
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
|
||||
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
|
||||
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);
|
||||
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
|
||||
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription(
|
||||
Arg.Is(subscriptionId),
|
||||
Arg.Is<SubscriptionUpdateOptions>(o =>
|
||||
o.Items.Count == 2 &&
|
||||
o.Items[0].Id == passwordManagerItemId &&
|
||||
o.Items[0].Price == familiesPlan.PasswordManager.StripePlanId &&
|
||||
o.Items[1].Id == seatAddOnItemId &&
|
||||
o.Items[1].Deleted == true &&
|
||||
o.Discounts.Count == 1 &&
|
||||
o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&
|
||||
o.ProrationBehavior == ProrationBehavior.None));
|
||||
|
||||
await _organizationRepository.Received(1).ReplaceAsync(
|
||||
Arg.Is<Organization>(org =>
|
||||
org.Id == _organizationId &&
|
||||
org.PlanType == PlanType.FamiliesAnnually &&
|
||||
org.Plan == familiesPlan.Name &&
|
||||
org.UsersGetPremium == familiesPlan.UsersGetPremium &&
|
||||
org.Seats == familiesPlan.PasswordManager.BaseSeats));
|
||||
|
||||
await _mailer.Received(1).SendEmail(
|
||||
Arg.Is<UpdatedInvoiceUpcomingMail>(email =>
|
||||
email.ToEmails.Contains("org@example.com") &&
|
||||
email.Subject == "Your Subscription Will Renew Soon"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WhenMilestone3Enabled_AndSeatAddOnWithQuantityOne_DeletesItem()
|
||||
{
|
||||
// Arrange
|
||||
var parsedEvent = new Event { Id = "evt_123", Type = "invoice.upcoming" };
|
||||
var customerId = "cus_123";
|
||||
var subscriptionId = "sub_123";
|
||||
var passwordManagerItemId = "si_pm_123";
|
||||
var seatAddOnItemId = "si_seat_123";
|
||||
|
||||
var invoice = new Invoice
|
||||
{
|
||||
CustomerId = customerId,
|
||||
AmountDue = 40000,
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
}
|
||||
};
|
||||
|
||||
var families2019Plan = new Families2019Plan();
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = subscriptionId,
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = passwordManagerItemId,
|
||||
Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = seatAddOnItemId,
|
||||
Price = new Price { Id = "personal-org-seat-annually" },
|
||||
Quantity = 1
|
||||
}
|
||||
}
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = _organizationId,
|
||||
BillingEmail = "org@example.com",
|
||||
PlanType = PlanType.FamiliesAnnually2019
|
||||
};
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
_stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
|
||||
_stripeEventUtilityService
|
||||
.GetIdsFromMetadata(subscription.Metadata)
|
||||
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
|
||||
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
|
||||
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);
|
||||
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
|
||||
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription(
|
||||
Arg.Is(subscriptionId),
|
||||
Arg.Is<SubscriptionUpdateOptions>(o =>
|
||||
o.Items.Count == 2 &&
|
||||
o.Items[0].Id == passwordManagerItemId &&
|
||||
o.Items[0].Price == familiesPlan.PasswordManager.StripePlanId &&
|
||||
o.Items[1].Id == seatAddOnItemId &&
|
||||
o.Items[1].Deleted == true &&
|
||||
o.Discounts.Count == 1 &&
|
||||
o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&
|
||||
o.ProrationBehavior == ProrationBehavior.None));
|
||||
|
||||
await _organizationRepository.Received(1).ReplaceAsync(
|
||||
Arg.Is<Organization>(org =>
|
||||
org.Id == _organizationId &&
|
||||
org.PlanType == PlanType.FamiliesAnnually &&
|
||||
org.Plan == familiesPlan.Name &&
|
||||
org.UsersGetPremium == familiesPlan.UsersGetPremium &&
|
||||
org.Seats == familiesPlan.PasswordManager.BaseSeats));
|
||||
|
||||
await _mailer.Received(1).SendEmail(
|
||||
Arg.Is<UpdatedInvoiceUpcomingMail>(email =>
|
||||
email.ToEmails.Contains("org@example.com") &&
|
||||
email.Subject == "Your Subscription Will Renew Soon"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WhenMilestone3Enabled_WithPremiumAccessAndSeatAddOn_UpdatesBothItems()
|
||||
{
|
||||
// Arrange
|
||||
var parsedEvent = new Event { Id = "evt_123", Type = "invoice.upcoming" };
|
||||
var customerId = "cus_123";
|
||||
var subscriptionId = "sub_123";
|
||||
var passwordManagerItemId = "si_pm_123";
|
||||
var premiumAccessItemId = "si_premium_123";
|
||||
var seatAddOnItemId = "si_seat_123";
|
||||
|
||||
var invoice = new Invoice
|
||||
{
|
||||
CustomerId = customerId,
|
||||
AmountDue = 40000,
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
}
|
||||
};
|
||||
|
||||
var families2019Plan = new Families2019Plan();
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = subscriptionId,
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = passwordManagerItemId,
|
||||
Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = premiumAccessItemId,
|
||||
Price = new Price { Id = families2019Plan.PasswordManager.StripePremiumAccessPlanId }
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = seatAddOnItemId,
|
||||
Price = new Price { Id = "personal-org-seat-annually" },
|
||||
Quantity = 2
|
||||
}
|
||||
}
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = _organizationId,
|
||||
BillingEmail = "org@example.com",
|
||||
PlanType = PlanType.FamiliesAnnually2019
|
||||
};
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
_stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
|
||||
_stripeEventUtilityService
|
||||
.GetIdsFromMetadata(subscription.Metadata)
|
||||
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
|
||||
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
|
||||
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);
|
||||
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
|
||||
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription(
|
||||
Arg.Is(subscriptionId),
|
||||
Arg.Is<SubscriptionUpdateOptions>(o =>
|
||||
o.Items.Count == 3 &&
|
||||
o.Items[0].Id == passwordManagerItemId &&
|
||||
o.Items[0].Price == familiesPlan.PasswordManager.StripePlanId &&
|
||||
o.Items[1].Id == premiumAccessItemId &&
|
||||
o.Items[1].Deleted == true &&
|
||||
o.Items[2].Id == seatAddOnItemId &&
|
||||
o.Items[2].Deleted == true &&
|
||||
o.Discounts.Count == 1 &&
|
||||
o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&
|
||||
o.ProrationBehavior == ProrationBehavior.None));
|
||||
|
||||
await _organizationRepository.Received(1).ReplaceAsync(
|
||||
Arg.Is<Organization>(org =>
|
||||
org.Id == _organizationId &&
|
||||
org.PlanType == PlanType.FamiliesAnnually &&
|
||||
org.Plan == familiesPlan.Name &&
|
||||
org.UsersGetPremium == familiesPlan.UsersGetPremium &&
|
||||
org.Seats == familiesPlan.PasswordManager.BaseSeats));
|
||||
|
||||
await _mailer.Received(1).SendEmail(
|
||||
Arg.Is<UpdatedInvoiceUpcomingMail>(email =>
|
||||
email.ToEmails.Contains("org@example.com") &&
|
||||
email.Subject == "Your Subscription Will Renew Soon"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user