mirror of
https://github.com/bitwarden/server.git
synced 2025-12-11 04:34:37 -06:00
[PM-28250] Fix seat add on defect (#6580)
* Handle seat add on * Remove old price instead
This commit is contained in:
parent
1274fe6562
commit
c620ec2aca
@ -220,7 +220,8 @@ public class UpcomingInvoiceHandler(
|
|||||||
[
|
[
|
||||||
new SubscriptionItemOptions
|
new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Id = passwordManagerItem.Id, Price = families.PasswordManager.StripePlanId
|
Id = passwordManagerItem.Id,
|
||||||
|
Price = families.PasswordManager.StripePlanId
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
Discounts =
|
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
|
try
|
||||||
{
|
{
|
||||||
await organizationRepository.ReplaceAsync(organization);
|
await organizationRepository.ReplaceAsync(organization);
|
||||||
|
|||||||
@ -1469,4 +1469,324 @@ public class UpcomingInvoiceHandlerTests
|
|||||||
email.ToEmails.Contains("org@example.com") &&
|
email.ToEmails.Contains("org@example.com") &&
|
||||||
email.Subject == "Your Subscription Will Renew Soon"));
|
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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user