1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-01 16:12:49 -05:00

Use upgrade path to change sponsorship

Sponsorships need to be annual to match the GB add-on charge rate
This commit is contained in:
Matt Gibson
2021-11-10 14:10:30 -05:00
committed by Justin Baur
parent a2467ea6ea
commit 9ec8bbb8bb
7 changed files with 178 additions and 243 deletions

View File

@ -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<Stripe.TaxRate> _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);
}
}

View File

@ -1,14 +1,12 @@
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Stripe; using Stripe;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Bit.Core.Models.Business namespace Bit.Core.Models.Business
{ {
public class OrganizationSubscriptionOptionsBase : Stripe.SubscriptionCreateOptions public class OrganizationSubscriptionOptionsBase : Stripe.SubscriptionCreateOptions
{ {
public OrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, public OrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, TaxInfo taxInfo, int additionalSeats, int additionalStorageGb, bool premiumAccessAddon)
int additionalSeats, int additionalStorageGb, bool premiumAccessAddon)
{ {
Items = new List<SubscriptionItemOptions>(); Items = new List<SubscriptionItemOptions>();
Metadata = new Dictionary<string, string> Metadata = new Dictionary<string, string>
@ -16,6 +14,15 @@ namespace Bit.Core.Models.Business
[org.GatewayIdField()] = org.Id.ToString() [org.GatewayIdField()] = org.Id.ToString()
}; };
if (plan.StripePlanId != null)
{
Items.Add(new SubscriptionItemOptions
{
Plan = plan.StripePlanId,
Quantity = 1
});
}
if (additionalSeats > 0 && plan.StripeSeatPlanId != null) if (additionalSeats > 0 && plan.StripeSeatPlanId != null)
{ {
Items.Add(new SubscriptionItemOptions Items.Add(new SubscriptionItemOptions
@ -42,53 +49,15 @@ namespace Bit.Core.Models.Business
Quantity = 1 Quantity = 1
}); });
} }
}
protected void AddPlanItem(StaticStore.Plan plan) => AddPlanItem(plan.StripePlanId); if (!string.IsNullOrWhiteSpace(taxInfo?.StripeTaxRateId))
protected void AddPlanItem(StaticStore.SponsoredPlan sponsoredPlan) => AddPlanItem(sponsoredPlan.StripePlanId);
protected void AddPlanItem(string stripePlanId)
{
if (stripePlanId != null)
{ {
Items.Add(new SubscriptionItemOptions DefaultTaxRates = new List<string> { taxInfo.StripeTaxRateId };
{
Plan = stripePlanId,
Quantity = 1,
});
}
}
protected void AddTaxRateItem(TaxInfo taxInfo) => AddTaxRateItem(new List<string> { taxInfo.StripeTaxRateId });
protected void AddTaxRateItem(List<Stripe.TaxRate> taxRates) => AddTaxRateItem(taxRates?.Select(t => t.Id).ToList());
protected void AddTaxRateItem(List<string> taxRateIds)
{
if (taxRateIds != null && taxRateIds.Any(tax => !string.IsNullOrWhiteSpace(tax)))
{
DefaultTaxRates = taxRateIds;
} }
} }
} }
public abstract class UnsponsoredOrganizationSubscriptionOptionsBase : OrganizationSubscriptionOptionsBase public class OrganizationPurchaseSubscriptionOptions : 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<Stripe.TaxRate> taxInfo,
int additionalSeats, int additionalStorage, bool premiumAccessAddon) :
base(org, plan, additionalSeats, additionalStorage, premiumAccessAddon)
{
AddPlanItem(plan);
AddTaxRateItem(taxInfo);
}
}
public class OrganizationPurchaseSubscriptionOptions : UnsponsoredOrganizationSubscriptionOptionsBase
{ {
public OrganizationPurchaseSubscriptionOptions( public OrganizationPurchaseSubscriptionOptions(
Organization org, StaticStore.Plan plan, Organization org, StaticStore.Plan plan,
@ -101,54 +70,16 @@ namespace Bit.Core.Models.Business
} }
} }
public class OrganizationUpgradeSubscriptionOptions : UnsponsoredOrganizationSubscriptionOptionsBase public class OrganizationUpgradeSubscriptionOptions : OrganizationSubscriptionOptionsBase
{ {
public OrganizationUpgradeSubscriptionOptions( public OrganizationUpgradeSubscriptionOptions(
string customerId, Organization org, string customerId, Organization org,
StaticStore.Plan plan, TaxInfo taxInfo, StaticStore.Plan plan, TaxInfo taxInfo,
int additionalSeats = 0, int additionalStorageGb = 0, 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<Stripe.TaxRate> taxInfo,
int additionalSeats = 0, int additionalStorageGb = 0,
bool premiumAccessAddon = false) : bool premiumAccessAddon = false) :
base(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon) base(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon)
{ {
Customer = customerId; Customer = customerId;
} }
} }
public class RemoveOrganizationSubscriptionOptions : OrganizationSubscriptionOptionsBase
{
public RemoveOrganizationSubscriptionOptions(string customerId, Organization org,
StaticStore.Plan plan, List<string> 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<Stripe.TaxRate> existingTaxRates, int additionalSeats = 0,
int additionalStorageGb = 0, bool premiumAccessAddon = false) :
base(org, existingPlan, additionalSeats, additionalStorageGb, premiumAccessAddon)
{
Customer = customerId;
AddPlanItem(sponsorshipPlan);
AddTaxRateItem(existingTaxRates);
}
}
} }

View File

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Stripe; using Stripe;
@ -6,16 +7,28 @@ namespace Bit.Core.Models.Business
{ {
public abstract class SubscriptionUpdate public abstract class SubscriptionUpdate
{ {
protected abstract string PlanId { get; } protected abstract List<string> PlanIds { get; }
public abstract SubscriptionItemOptions RevertItemOptions(Subscription subscription); public abstract List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription);
public abstract SubscriptionItemOptions UpgradeItemOptions(Subscription subscription); public abstract List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription);
public bool UpdateNeeded(Subscription subscription) => public bool UpdateNeeded(Subscription subscription)
(SubscriptionItem(subscription)?.Quantity ?? 0) != (UpgradeItemOptions(subscription).Quantity ?? 0); {
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) => protected static SubscriptionItem SubscriptionItem(Subscription subscription, string planId) =>
subscription.Items?.Data?.FirstOrDefault(i => i.Plan.Id == 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 Organization _organization;
private readonly StaticStore.Plan _plan; private readonly StaticStore.Plan _plan;
private readonly long? _additionalSeats; private readonly long? _additionalSeats;
protected override string PlanId => _plan.StripeSeatPlanId; protected override List<string> PlanIds => new() { _plan.StripeSeatPlanId };
public SeatSubscriptionUpdate(Organization organization, StaticStore.Plan plan, long? additionalSeats) public SeatSubscriptionUpdate(Organization organization, StaticStore.Plan plan, long? additionalSeats)
{ {
@ -33,27 +46,33 @@ namespace Bit.Core.Models.Business
_additionalSeats = additionalSeats; _additionalSeats = additionalSeats;
} }
public override SubscriptionItemOptions UpgradeItemOptions(Subscription subscription) public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
{ {
var item = SubscriptionItem(subscription); var item = SubscriptionItem(subscription, PlanIds.Single());
return new SubscriptionItemOptions return new()
{ {
Id = item?.Id, new SubscriptionItemOptions
Plan = PlanId, {
Quantity = _additionalSeats, Id = item?.Id,
Deleted = (item?.Id != null && _additionalSeats == 0) ? true : (bool?)null, Plan = PlanIds.Single(),
Quantity = _additionalSeats,
Deleted = (item?.Id != null && _additionalSeats == 0) ? true : (bool?)null,
}
}; };
} }
public override SubscriptionItemOptions RevertItemOptions(Subscription subscription) public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)
{ {
var item = SubscriptionItem(subscription); var item = SubscriptionItem(subscription, PlanIds.Single());
return new SubscriptionItemOptions return new()
{ {
Id = item?.Id, new SubscriptionItemOptions
Plan = PlanId, {
Quantity = _organization.Seats, Id = item?.Id,
Deleted = item?.Id != null ? true : (bool?)null, 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 string _plan;
private readonly long? _additionalStorage; private readonly long? _additionalStorage;
protected override string PlanId => _plan; protected override List<string> PlanIds => new() { _plan };
public StorageSubscriptionUpdate(string plan, long? additionalStorage) public StorageSubscriptionUpdate(string plan, long? additionalStorage)
{ {
@ -70,28 +89,102 @@ namespace Bit.Core.Models.Business
_additionalStorage = additionalStorage; _additionalStorage = additionalStorage;
} }
public override SubscriptionItemOptions UpgradeItemOptions(Subscription subscription) public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
{ {
var item = SubscriptionItem(subscription); var item = SubscriptionItem(subscription, PlanIds.Single());
return new SubscriptionItemOptions return new()
{ {
Id = item?.Id, new SubscriptionItemOptions
Plan = _plan, {
Quantity = _additionalStorage, Id = item?.Id,
Deleted = (item?.Id != null && _additionalStorage == 0) ? true : (bool?)null, Plan = _plan,
Quantity = _additionalStorage,
Deleted = (item?.Id != null && _additionalStorage == 0) ? true : (bool?)null,
}
}; };
} }
public override SubscriptionItemOptions RevertItemOptions(Subscription subscription) public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)
{ {
var item = SubscriptionItem(subscription); var item = SubscriptionItem(subscription, PlanIds.Single());
return new SubscriptionItemOptions return new()
{ {
Id = item?.Id, new SubscriptionItemOptions
Plan = _plan, {
Quantity = item?.Quantity ?? 0, Id = item?.Id,
Deleted = item?.Id != null ? true : (bool?)null, 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<string> PlanIds => new() { _existingPlanStripeId, _sponsoredPlanStripeId };
public SponsorOrganizationSubscriptionUpdate(StaticStore.Plan existingPlan, StaticStore.SponsoredPlan sponsoredPlan, bool applySponsorship)
{
_existingPlanStripeId = existingPlan.StripePlanId;
_sponsoredPlanStripeId = sponsoredPlan.StripePlanId;
}
public override List<SubscriptionItemOptions> 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<SubscriptionItemOptions> 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);
}
} }

View File

@ -14,7 +14,7 @@ namespace Bit.Core.Services
string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats, string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats,
bool premiumAccessAddon, TaxInfo taxInfo); bool premiumAccessAddon, TaxInfo taxInfo);
Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship); Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship);
Task<bool> RemoveOrganizationSponsorshipAsync(Organization org); Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship);
Task<string> UpgradeFreeOrganizationAsync(Organization org, Plan plan, Task<string> UpgradeFreeOrganizationAsync(Organization org, Plan plan,
short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo); short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo);
Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,

View File

@ -128,7 +128,8 @@ namespace Bit.Core.Services
if (existingSponsorship == null) if (existingSponsorship == null)
{ {
await RemoveSponsorshipAsync(sponsoredOrganization); // TODO: null safe this method
await RemoveSponsorshipAsync(sponsoredOrganization, null);
// TODO on fail, mark org as disabled. // TODO on fail, mark org as disabled.
return false; return false;
} }
@ -136,7 +137,7 @@ namespace Bit.Core.Services
var validated = true; var validated = true;
if (existingSponsorship.SponsoringOrganizationId == null || existingSponsorship.SponsoringOrganizationUserId == null) if (existingSponsorship.SponsoringOrganizationId == null || existingSponsorship.SponsoringOrganizationUserId == null)
{ {
await RemoveSponsorshipAsync(sponsoredOrganization); await RemoveSponsorshipAsync(sponsoredOrganization, existingSponsorship);
validated = false; validated = false;
} }
@ -144,7 +145,7 @@ namespace Bit.Core.Services
.GetByIdAsync(existingSponsorship.SponsoringOrganizationId.Value); .GetByIdAsync(existingSponsorship.SponsoringOrganizationId.Value);
if (!sponsoringOrganization.Enabled) if (!sponsoringOrganization.Enabled)
{ {
await RemoveSponsorshipAsync(sponsoredOrganization); await RemoveSponsorshipAsync(sponsoredOrganization, existingSponsorship);
validated = false; validated = false;
} }
@ -166,7 +167,7 @@ namespace Bit.Core.Services
public async Task RemoveSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship = null) public async Task RemoveSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship = null)
{ {
var success = await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization); await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization, sponsorship);
await _organizationRepository.UpsertAsync(sponsoredOrganization); await _organizationRepository.UpsertAsync(sponsoredOrganization);
if (sponsorship == null) if (sponsorship == null)
@ -174,49 +175,22 @@ namespace Bit.Core.Services
return; return;
} }
if (success) // Initialize the record as available
{ sponsorship.SponsoredOrganizationId = null;
// Initialize the record as available sponsorship.FriendlyName = null;
sponsorship.SponsoredOrganizationId = null; sponsorship.OfferedToEmail = null;
sponsorship.FriendlyName = null; sponsorship.PlanSponsorshipType = null;
sponsorship.OfferedToEmail = null; sponsorship.TimesRenewedWithoutValidation = 0;
sponsorship.PlanSponsorshipType = null; sponsorship.SponsorshipLapsedDate = null;
sponsorship.TimesRenewedWithoutValidation = 0;
sponsorship.SponsorshipLapsedDate = null;
if (sponsorship.CloudSponsor || sponsorship.SponsorshipLapsedDate.HasValue) if (sponsorship.CloudSponsor || sponsorship.SponsorshipLapsedDate.HasValue)
{ {
await _organizationSponsorshipRepository.DeleteAsync(sponsorship); await _organizationSponsorshipRepository.DeleteAsync(sponsorship);
}
else
{
await _organizationSponsorshipRepository.UpsertAsync(sponsorship);
}
} }
else 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); await _organizationSponsorshipRepository.UpsertAsync(sponsorship);
} }
} }
} }

View File

@ -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); 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<bool> RemoveOrganizationSponsorshipAsync(Organization org) public Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship) =>
{ ChangeOrganizationSponsorship(org, sponsorship, true);
var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId);
var sub = await _stripeAdapter.SubscriptionGetAsync(org.GatewaySubscriptionId);
var sponsoredSubscription = new SponsoredOrganizationSubscription(org, sub); public Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship) =>
var subCreateOptions = sponsoredSubscription.RemoveOrganizationSubscriptionOptions(); ChangeOrganizationSponsorship(org, sponsorship, false);
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 async Task<string> UpgradeFreeOrganizationAsync(Organization org, StaticStore.Plan plan, public async Task<string> UpgradeFreeOrganizationAsync(Organization org, StaticStore.Plan plan,
short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo) short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo)
@ -736,11 +718,11 @@ namespace Bit.Core.Services
var collectionMethod = sub.CollectionMethod; var collectionMethod = sub.CollectionMethod;
var daysUntilDue = sub.DaysUntilDue; var daysUntilDue = sub.DaysUntilDue;
var chargeNow = collectionMethod == "charge_automatically"; var chargeNow = collectionMethod == "charge_automatically";
var updatedItemOptions = subscriptionUpdate.UpgradeItemOptions(sub); var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
var subUpdateOptions = new Stripe.SubscriptionUpdateOptions var subUpdateOptions = new Stripe.SubscriptionUpdateOptions
{ {
Items = new List<Stripe.SubscriptionItemOptions> { updatedItemOptions }, Items = updatedItemOptions,
ProrationBehavior = "always_invoice", ProrationBehavior = "always_invoice",
DaysUntilDue = daysUntilDue ?? 1, DaysUntilDue = daysUntilDue ?? 1,
CollectionMethod = "send_invoice", CollectionMethod = "send_invoice",
@ -783,14 +765,8 @@ namespace Bit.Core.Services
throw new BadRequestException("Unable to locate draft invoice for subscription update."); 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; string paymentIntentClientSecret = null;
if (updatedItemOptions.Quantity > 0) if (invoice.AmountDue > 0 && updatedItemOptions.Any(i => i.Quantity > 0))
{ {
try try
{ {
@ -814,7 +790,7 @@ namespace Bit.Core.Services
// Need to revert the subscription // Need to revert the subscription
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
{ {
Items = new List<Stripe.SubscriptionItemOptions> { subscriptionUpdate.RevertItemOptions(sub) }, Items = subscriptionUpdate.RevertItemsOptions(sub),
// This proration behavior prevents a false "credit" from // This proration behavior prevents a false "credit" from
// being applied forward to the next month's invoice // being applied forward to the next month's invoice
ProrationBehavior = "none", ProrationBehavior = "none",

View File

@ -484,7 +484,7 @@ namespace Bit.Core.Utilities
PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise, PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise,
SponsoredProductType = ProductType.Families, SponsoredProductType = ProductType.Families,
SponsoringProductType = ProductType.Enterprise, SponsoringProductType = ProductType.Enterprise,
StripePlanId = "2021-enterprise-sponsored-families-org-monthly", StripePlanId = "2021-family-for-enterprise-annually",
UsersCanSponsor = (OrganizationUserOrganizationDetails org) => UsersCanSponsor = (OrganizationUserOrganizationDetails org) =>
GetPlan(org.PlanType).Product == ProductType.Enterprise, GetPlan(org.PlanType).Product == ProductType.Enterprise,
} }