From 339248ffaf701a845fd8eca653df2e3f7b11fbab Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 10 Nov 2021 14:10:30 -0500 Subject: [PATCH] Use upgrade path to change sponsorship Sponsorships need to be annual to match the GB add-on charge rate --- .../SponsoredOrganizationSubscription.cs | 39 ---- .../Business/SubscriptionCreateOptions.cs | 97 ++-------- .../Models/Business/SubscriptionUpdate.cs | 167 ++++++++++++++---- src/Core/Services/IPaymentService.cs | 2 +- .../OrganizationSponsorshipService.cs | 56 ++---- .../Implementations/StripePaymentService.cs | 58 ++---- src/Core/Utilities/StaticStore.cs | 2 +- 7 files changed, 178 insertions(+), 243 deletions(-) delete mode 100644 src/Core/Models/Business/SponsoredOrganizationSubscription.cs diff --git a/src/Core/Models/Business/SponsoredOrganizationSubscription.cs b/src/Core/Models/Business/SponsoredOrganizationSubscription.cs deleted file mode 100644 index 30ea3e047..000000000 --- a/src/Core/Models/Business/SponsoredOrganizationSubscription.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections.Generic; -using Bit.Core.Models.Table; - -namespace Bit.Core.Models.Business -{ - public class SponsoredOrganizationSubscription - { - public const string OrganizationSponsorhipIdMetadataKey = "OrganizationSponsorshipId"; - private readonly string _customerId; - private readonly Organization _org; - private readonly StaticStore.Plan _plan; - private readonly List _taxRates; - - public SponsoredOrganizationSubscription(Organization org, Stripe.Subscription existingSubscription) - { - _org = org; - _customerId = org.GatewayCustomerId; - _plan = Utilities.StaticStore.GetPlan(org.PlanType); - _taxRates = existingSubscription.DefaultTaxRates; - } - - public SponsorOrganizationSubscriptionOptions GetSponsorSubscriptionOptions(OrganizationSponsorship sponsorship, - int additionalSeats = 0, int additionalStorageGb = 0, bool premiumAccessAddon = false) - { - var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value); - - var subCreateOptions = new SponsorOrganizationSubscriptionOptions(_customerId, _org, _plan, - sponsoredPlan, _taxRates, additionalSeats, additionalStorageGb, premiumAccessAddon); - - subCreateOptions.Metadata.Add(OrganizationSponsorhipIdMetadataKey, sponsorship.Id.ToString()); - return subCreateOptions; - } - - public OrganizationUpgradeSubscriptionOptions RemoveOrganizationSubscriptionOptions(int additionalSeats = 0, - int additionalStorageGb = 0, bool premiumAccessAddon = false) => - new OrganizationUpgradeSubscriptionOptions(_customerId, _org, _plan, _taxRates, - additionalSeats, additionalStorageGb, premiumAccessAddon); - } -} diff --git a/src/Core/Models/Business/SubscriptionCreateOptions.cs b/src/Core/Models/Business/SubscriptionCreateOptions.cs index 2cc01fb68..75086c654 100644 --- a/src/Core/Models/Business/SubscriptionCreateOptions.cs +++ b/src/Core/Models/Business/SubscriptionCreateOptions.cs @@ -1,14 +1,12 @@ using Bit.Core.Models.Table; using Stripe; using System.Collections.Generic; -using System.Linq; namespace Bit.Core.Models.Business { public class OrganizationSubscriptionOptionsBase : Stripe.SubscriptionCreateOptions { - public OrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, - int additionalSeats, int additionalStorageGb, bool premiumAccessAddon) + public OrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, TaxInfo taxInfo, int additionalSeats, int additionalStorageGb, bool premiumAccessAddon) { Items = new List(); Metadata = new Dictionary @@ -16,6 +14,15 @@ namespace Bit.Core.Models.Business [org.GatewayIdField()] = org.Id.ToString() }; + if (plan.StripePlanId != null) + { + Items.Add(new SubscriptionItemOptions + { + Plan = plan.StripePlanId, + Quantity = 1 + }); + } + if (additionalSeats > 0 && plan.StripeSeatPlanId != null) { Items.Add(new SubscriptionItemOptions @@ -42,53 +49,15 @@ namespace Bit.Core.Models.Business Quantity = 1 }); } - } - protected void AddPlanItem(StaticStore.Plan plan) => AddPlanItem(plan.StripePlanId); - protected void AddPlanItem(StaticStore.SponsoredPlan sponsoredPlan) => AddPlanItem(sponsoredPlan.StripePlanId); - protected void AddPlanItem(string stripePlanId) - { - if (stripePlanId != null) + if (!string.IsNullOrWhiteSpace(taxInfo?.StripeTaxRateId)) { - Items.Add(new SubscriptionItemOptions - { - Plan = stripePlanId, - Quantity = 1, - }); - } - } - - protected void AddTaxRateItem(TaxInfo taxInfo) => AddTaxRateItem(new List { taxInfo.StripeTaxRateId }); - protected void AddTaxRateItem(List taxRates) => AddTaxRateItem(taxRates?.Select(t => t.Id).ToList()); - protected void AddTaxRateItem(List taxRateIds) - { - if (taxRateIds != null && taxRateIds.Any(tax => !string.IsNullOrWhiteSpace(tax))) - { - DefaultTaxRates = taxRateIds; + DefaultTaxRates = new List { taxInfo.StripeTaxRateId }; } } } - public abstract class UnsponsoredOrganizationSubscriptionOptionsBase : OrganizationSubscriptionOptionsBase - { - public UnsponsoredOrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, TaxInfo taxInfo, - int additionalSeats, int additionalStorage, bool premiumAccessAddon) : - base(org, plan, additionalSeats, additionalStorage, premiumAccessAddon) - { - AddPlanItem(plan); - AddTaxRateItem(taxInfo); - } - public UnsponsoredOrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, List taxInfo, - int additionalSeats, int additionalStorage, bool premiumAccessAddon) : - base(org, plan, additionalSeats, additionalStorage, premiumAccessAddon) - { - AddPlanItem(plan); - AddTaxRateItem(taxInfo); - } - - } - - public class OrganizationPurchaseSubscriptionOptions : UnsponsoredOrganizationSubscriptionOptionsBase + public class OrganizationPurchaseSubscriptionOptions : OrganizationSubscriptionOptionsBase { public OrganizationPurchaseSubscriptionOptions( Organization org, StaticStore.Plan plan, @@ -101,54 +70,16 @@ namespace Bit.Core.Models.Business } } - public class OrganizationUpgradeSubscriptionOptions : UnsponsoredOrganizationSubscriptionOptionsBase + public class OrganizationUpgradeSubscriptionOptions : OrganizationSubscriptionOptionsBase { public OrganizationUpgradeSubscriptionOptions( string customerId, Organization org, StaticStore.Plan plan, TaxInfo taxInfo, int additionalSeats = 0, int additionalStorageGb = 0, - bool premiumAccessAddon = false) : - base(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon) - { - Customer = customerId; - } - public OrganizationUpgradeSubscriptionOptions( - string customerId, Organization org, - StaticStore.Plan plan, List taxInfo, - int additionalSeats = 0, int additionalStorageGb = 0, bool premiumAccessAddon = false) : base(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon) { Customer = customerId; } } - - public class RemoveOrganizationSubscriptionOptions : OrganizationSubscriptionOptionsBase - { - public RemoveOrganizationSubscriptionOptions(string customerId, Organization org, - StaticStore.Plan plan, List existingTaxRateStripeIds, - int additionalSeats = 0, int additionalStorageGb = 0, - bool premiumAccessAddon = false) : - base(org, plan, additionalSeats, additionalStorageGb, premiumAccessAddon) - { - Customer = customerId; - AddPlanItem(plan); - AddTaxRateItem(existingTaxRateStripeIds); - } - - } - - public class SponsorOrganizationSubscriptionOptions : OrganizationSubscriptionOptionsBase - { - public SponsorOrganizationSubscriptionOptions( - string customerId, Organization org, StaticStore.Plan existingPlan, - StaticStore.SponsoredPlan sponsorshipPlan, List existingTaxRates, int additionalSeats = 0, - int additionalStorageGb = 0, bool premiumAccessAddon = false) : - base(org, existingPlan, additionalSeats, additionalStorageGb, premiumAccessAddon) - { - Customer = customerId; - AddPlanItem(sponsorshipPlan); - AddTaxRateItem(existingTaxRates); - } - } } diff --git a/src/Core/Models/Business/SubscriptionUpdate.cs b/src/Core/Models/Business/SubscriptionUpdate.cs index f4aaedf8b..4f0b2cc2c 100644 --- a/src/Core/Models/Business/SubscriptionUpdate.cs +++ b/src/Core/Models/Business/SubscriptionUpdate.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using Bit.Core.Models.Table; using Stripe; @@ -6,16 +7,28 @@ namespace Bit.Core.Models.Business { public abstract class SubscriptionUpdate { - protected abstract string PlanId { get; } + protected abstract List PlanIds { get; } - public abstract SubscriptionItemOptions RevertItemOptions(Subscription subscription); - public abstract SubscriptionItemOptions UpgradeItemOptions(Subscription subscription); + public abstract List RevertItemsOptions(Subscription subscription); + public abstract List UpgradeItemsOptions(Subscription subscription); - public bool UpdateNeeded(Subscription subscription) => - (SubscriptionItem(subscription)?.Quantity ?? 0) != (UpgradeItemOptions(subscription).Quantity ?? 0); + public bool UpdateNeeded(Subscription subscription) + { + var upgradeItemsOptions = UpgradeItemsOptions(subscription); + foreach (var upgradeItemOptions in upgradeItemsOptions) + { + var upgradeQuantity = upgradeItemOptions.Quantity ?? 0; + var existingQuantity = SubscriptionItem(subscription, upgradeItemOptions.Plan)?.Quantity ?? 0; + if (upgradeQuantity != existingQuantity) + { + return true; + } + } + return false; + } - protected SubscriptionItem SubscriptionItem(Subscription subscription) => - subscription.Items?.Data?.FirstOrDefault(i => i.Plan.Id == PlanId); + protected static SubscriptionItem SubscriptionItem(Subscription subscription, string planId) => + subscription.Items?.Data?.FirstOrDefault(i => i.Plan.Id == planId); } @@ -24,7 +37,7 @@ namespace Bit.Core.Models.Business private readonly Organization _organization; private readonly StaticStore.Plan _plan; private readonly long? _additionalSeats; - protected override string PlanId => _plan.StripeSeatPlanId; + protected override List PlanIds => new() { _plan.StripeSeatPlanId }; public SeatSubscriptionUpdate(Organization organization, StaticStore.Plan plan, long? additionalSeats) { @@ -33,27 +46,33 @@ namespace Bit.Core.Models.Business _additionalSeats = additionalSeats; } - public override SubscriptionItemOptions UpgradeItemOptions(Subscription subscription) + public override List UpgradeItemsOptions(Subscription subscription) { - var item = SubscriptionItem(subscription); - return new SubscriptionItemOptions + var item = SubscriptionItem(subscription, PlanIds.Single()); + return new() { - Id = item?.Id, - Plan = PlanId, - Quantity = _additionalSeats, - Deleted = (item?.Id != null && _additionalSeats == 0) ? true : (bool?)null, + new SubscriptionItemOptions + { + Id = item?.Id, + Plan = PlanIds.Single(), + Quantity = _additionalSeats, + Deleted = (item?.Id != null && _additionalSeats == 0) ? true : (bool?)null, + } }; } - public override SubscriptionItemOptions RevertItemOptions(Subscription subscription) + public override List RevertItemsOptions(Subscription subscription) { - var item = SubscriptionItem(subscription); - return new SubscriptionItemOptions + var item = SubscriptionItem(subscription, PlanIds.Single()); + return new() { - Id = item?.Id, - Plan = PlanId, - Quantity = _organization.Seats, - Deleted = item?.Id != null ? true : (bool?)null, + new SubscriptionItemOptions + { + Id = item?.Id, + Plan = PlanIds.Single(), + Quantity = _organization.Seats, + Deleted = item?.Id != null ? true : (bool?)null, + } }; } } @@ -62,7 +81,7 @@ namespace Bit.Core.Models.Business { private readonly string _plan; private readonly long? _additionalStorage; - protected override string PlanId => _plan; + protected override List PlanIds => new() { _plan }; public StorageSubscriptionUpdate(string plan, long? additionalStorage) { @@ -70,28 +89,102 @@ namespace Bit.Core.Models.Business _additionalStorage = additionalStorage; } - public override SubscriptionItemOptions UpgradeItemOptions(Subscription subscription) + public override List UpgradeItemsOptions(Subscription subscription) { - var item = SubscriptionItem(subscription); - return new SubscriptionItemOptions + var item = SubscriptionItem(subscription, PlanIds.Single()); + return new() { - Id = item?.Id, - Plan = _plan, - Quantity = _additionalStorage, - Deleted = (item?.Id != null && _additionalStorage == 0) ? true : (bool?)null, + new SubscriptionItemOptions + { + Id = item?.Id, + Plan = _plan, + Quantity = _additionalStorage, + Deleted = (item?.Id != null && _additionalStorage == 0) ? true : (bool?)null, + } }; } - public override SubscriptionItemOptions RevertItemOptions(Subscription subscription) + public override List RevertItemsOptions(Subscription subscription) { - var item = SubscriptionItem(subscription); - return new SubscriptionItemOptions + var item = SubscriptionItem(subscription, PlanIds.Single()); + return new() { - Id = item?.Id, - Plan = _plan, - Quantity = item?.Quantity ?? 0, - Deleted = item?.Id != null ? true : (bool?)null, + new SubscriptionItemOptions + { + Id = item?.Id, + Plan = _plan, + Quantity = item?.Quantity ?? 0, + Deleted = item?.Id != null ? true : (bool?)null, + } }; } } + + public class SponsorOrganizationSubscriptionUpdate : SubscriptionUpdate + { + private string _existingPlanStripeId; + private string _sponsoredPlanStripeId; + private bool _applySponsorship; + protected override List PlanIds => new() { _existingPlanStripeId, _sponsoredPlanStripeId }; + + public SponsorOrganizationSubscriptionUpdate(StaticStore.Plan existingPlan, StaticStore.SponsoredPlan sponsoredPlan, bool applySponsorship) + { + _existingPlanStripeId = existingPlan.StripePlanId; + _sponsoredPlanStripeId = sponsoredPlan.StripePlanId; + } + + public override List RevertItemsOptions(Subscription subscription) + { + return new() + { + new SubscriptionItemOptions + { + Id = AddStripeItem(subscription)?.Id, + Plan = AddStripePlanId, + Quantity = 0, + Deleted = true, + }, + new SubscriptionItemOptions + { + Id = RemoveStripeItem(subscription)?.Id, + Plan = RemoveStripePlanId, + Quantity = 1, + Deleted = false, + }, + }; + } + + public override List UpgradeItemsOptions(Subscription subscription) + { + return new() + { + new SubscriptionItemOptions + { + Id = RemoveStripeItem(subscription)?.Id, + Plan = RemoveStripePlanId, + Quantity = 0, + Deleted = true, + }, + new SubscriptionItemOptions + { + Id = AddStripeItem(subscription)?.Id, + Plan = AddStripePlanId, + Quantity = 1, + Deleted = false, + }, + }; + } + + private string RemoveStripePlanId => _applySponsorship ? _existingPlanStripeId : _sponsoredPlanStripeId; + private string AddStripePlanId => _applySponsorship ? _sponsoredPlanStripeId : _existingPlanStripeId; + private Stripe.SubscriptionItem RemoveStripeItem(Subscription subscription) => + _applySponsorship ? + SubscriptionItem(subscription, _existingPlanStripeId) : + SubscriptionItem(subscription, _sponsoredPlanStripeId); + private Stripe.SubscriptionItem AddStripeItem(Subscription subscription) => + _applySponsorship ? + SubscriptionItem(subscription, _sponsoredPlanStripeId) : + SubscriptionItem(subscription, _existingPlanStripeId); + + } } diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index 422793452..2bb515a21 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -14,7 +14,7 @@ namespace Bit.Core.Services string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo); Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship); - Task RemoveOrganizationSponsorshipAsync(Organization org); + Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship); Task UpgradeFreeOrganizationAsync(Organization org, Plan plan, short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo); Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, diff --git a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs index 6e870a88b..cac483f2e 100644 --- a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs +++ b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs @@ -128,7 +128,8 @@ namespace Bit.Core.Services if (existingSponsorship == null) { - await RemoveSponsorshipAsync(sponsoredOrganization); + // TODO: null safe this method + await RemoveSponsorshipAsync(sponsoredOrganization, null); // TODO on fail, mark org as disabled. return false; } @@ -136,7 +137,7 @@ namespace Bit.Core.Services var validated = true; if (existingSponsorship.SponsoringOrganizationId == null || existingSponsorship.SponsoringOrganizationUserId == null) { - await RemoveSponsorshipAsync(sponsoredOrganization); + await RemoveSponsorshipAsync(sponsoredOrganization, existingSponsorship); validated = false; } @@ -144,7 +145,7 @@ namespace Bit.Core.Services .GetByIdAsync(existingSponsorship.SponsoringOrganizationId.Value); if (!sponsoringOrganization.Enabled) { - await RemoveSponsorshipAsync(sponsoredOrganization); + await RemoveSponsorshipAsync(sponsoredOrganization, existingSponsorship); validated = false; } @@ -166,7 +167,7 @@ namespace Bit.Core.Services public async Task RemoveSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship = null) { - var success = await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization); + await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization, sponsorship); await _organizationRepository.UpsertAsync(sponsoredOrganization); if (sponsorship == null) @@ -174,49 +175,22 @@ namespace Bit.Core.Services return; } - if (success) - { - // Initialize the record as available - sponsorship.SponsoredOrganizationId = null; - sponsorship.FriendlyName = null; - sponsorship.OfferedToEmail = null; - sponsorship.PlanSponsorshipType = null; - sponsorship.TimesRenewedWithoutValidation = 0; - sponsorship.SponsorshipLapsedDate = null; + // Initialize the record as available + sponsorship.SponsoredOrganizationId = null; + sponsorship.FriendlyName = null; + sponsorship.OfferedToEmail = null; + sponsorship.PlanSponsorshipType = null; + sponsorship.TimesRenewedWithoutValidation = 0; + sponsorship.SponsorshipLapsedDate = null; - if (sponsorship.CloudSponsor || sponsorship.SponsorshipLapsedDate.HasValue) - { - await _organizationSponsorshipRepository.DeleteAsync(sponsorship); - } - else - { - await _organizationSponsorshipRepository.UpsertAsync(sponsorship); - } + if (sponsorship.CloudSponsor || sponsorship.SponsorshipLapsedDate.HasValue) + { + await _organizationSponsorshipRepository.DeleteAsync(sponsorship); } else { - sponsorship.SponsoringOrganizationId = null; - sponsorship.SponsoringOrganizationUserId = null; - - if (!sponsorship.CloudSponsor) - { - // Sef-hosted sponsorship record - // we need to make the existing sponsorship available, and add - // a new sponsorship record to record the lapsed sponsorship - var cleanSponsorship = new OrganizationSponsorship - { - InstallationId = sponsorship.InstallationId, - SponsoringOrganizationId = sponsorship.SponsoringOrganizationId, - SponsoringOrganizationUserId = sponsorship.SponsoringOrganizationUserId, - CloudSponsor = sponsorship.CloudSponsor, - }; - await _organizationSponsorshipRepository.UpsertAsync(cleanSponsorship); - } - - sponsorship.SponsorshipLapsedDate ??= DateTime.UtcNow; await _organizationSponsorshipRepository.UpsertAsync(sponsorship); } - } } diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index b4460d602..31aa08dc5 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -192,43 +192,25 @@ namespace Bit.Core.Services } } - public async Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship) + private async Task ChangeOrganizationSponsorship(Organization org, OrganizationSponsorship sponsorship, bool applySponsorship) { - var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId); + var existingPlan = Utilities.StaticStore.GetPlan(org.PlanType); + var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value); + var subscriptionUpdate = new SponsorOrganizationSubscriptionUpdate(existingPlan, sponsoredPlan, applySponsorship); + + var prorationTime = DateTime.UtcNow; + await FinalizeSubscriptionChangeAsync(org, subscriptionUpdate, prorationTime); + var sub = await _stripeAdapter.SubscriptionGetAsync(org.GatewaySubscriptionId); + org.ExpirationDate = sub.CurrentPeriodEnd; - var sponsoredSubscription = new SponsoredOrganizationSubscription(org, sub); - - var subscription = await ChargeForNewSubscriptionAsync(org, customer, false, - false, PaymentMethodType.None, sponsoredSubscription.GetSponsorSubscriptionOptions(sponsorship), null); - org.GatewaySubscriptionId = subscription.Id; - - org.ExpirationDate = subscription.CurrentPeriodEnd; } - public async Task RemoveOrganizationSponsorshipAsync(Organization org) - { - var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId); - var sub = await _stripeAdapter.SubscriptionGetAsync(org.GatewaySubscriptionId); + public Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship) => + ChangeOrganizationSponsorship(org, sponsorship, true); - var sponsoredSubscription = new SponsoredOrganizationSubscription(org, sub); - var subCreateOptions = sponsoredSubscription.RemoveOrganizationSubscriptionOptions(); - - var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions); - var subscription = await ChargeForNewSubscriptionAsync(org, customer, false, - stripePaymentMethod, paymentMethodType, subCreateOptions, null); - - if (subscription.Status == "incomplete") - { - // TODO: revert - return false; - } - org.GatewaySubscriptionId = subscription.Id; - org.Enabled = true; - org.ExpirationDate = subscription.CurrentPeriodEnd; - - return true; - } + public Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship) => + ChangeOrganizationSponsorship(org, sponsorship, false); public async Task UpgradeFreeOrganizationAsync(Organization org, StaticStore.Plan plan, short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo) @@ -736,11 +718,11 @@ namespace Bit.Core.Services var collectionMethod = sub.CollectionMethod; var daysUntilDue = sub.DaysUntilDue; var chargeNow = collectionMethod == "charge_automatically"; - var updatedItemOptions = subscriptionUpdate.UpgradeItemOptions(sub); + var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub); var subUpdateOptions = new Stripe.SubscriptionUpdateOptions { - Items = new List { updatedItemOptions }, + Items = updatedItemOptions, ProrationBehavior = "always_invoice", DaysUntilDue = daysUntilDue ?? 1, CollectionMethod = "send_invoice", @@ -783,14 +765,8 @@ namespace Bit.Core.Services throw new BadRequestException("Unable to locate draft invoice for subscription update."); } - // If no amount due, invoice is autofinalized, we're done - if (invoice.AmountDue <= 0) - { - return null; - } - string paymentIntentClientSecret = null; - if (updatedItemOptions.Quantity > 0) + if (invoice.AmountDue > 0 && updatedItemOptions.Any(i => i.Quantity > 0)) { try { @@ -814,7 +790,7 @@ namespace Bit.Core.Services // Need to revert the subscription await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions { - Items = new List { subscriptionUpdate.RevertItemOptions(sub) }, + Items = subscriptionUpdate.RevertItemsOptions(sub), // This proration behavior prevents a false "credit" from // being applied forward to the next month's invoice ProrationBehavior = "none", diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index 68a3e8857..d905bd0a7 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -484,7 +484,7 @@ namespace Bit.Core.Utilities PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise, SponsoredProductType = ProductType.Families, SponsoringProductType = ProductType.Enterprise, - StripePlanId = "2021-enterprise-sponsored-families-org-monthly", + StripePlanId = "2021-family-for-enterprise-annually", UsersCanSponsor = (OrganizationUserOrganizationDetails org) => GetPlan(org.PlanType).Product == ProductType.Enterprise, }