Skip to content

Commit

Permalink
[EC-144] Fix stripe revert logic (bitwarden#2014)
Browse files Browse the repository at this point in the history
* Revert scaling by previous value

* Throw is Stripe subscription revert fails

* Remove unused property

* Add null check to accommodate for not existing storage-gb-xxx subscription item

* Use long? instead of Nullable<long>

* Remove redundant try/catch

* Ensure collectionMethod is changed back, even when revertSub fails

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
  • Loading branch information
djsmith85 and MGibson1 authored May 31, 2022
1 parent 39ba68e commit 610be2c
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 48 deletions.
24 changes: 17 additions & 7 deletions src/Core/Models/Business/SubscriptionUpdate.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using Bit.Core.Entities;
using Stripe;
Expand Down Expand Up @@ -34,16 +35,17 @@ protected static SubscriptionItem SubscriptionItem(Subscription subscription, st

public class SeatSubscriptionUpdate : SubscriptionUpdate
{
private readonly Organization _organization;
private readonly int _previousSeats;
private readonly StaticStore.Plan _plan;
private readonly long? _additionalSeats;
protected override List<string> PlanIds => new() { _plan.StripeSeatPlanId };


public SeatSubscriptionUpdate(Organization organization, StaticStore.Plan plan, long? additionalSeats)
{
_organization = organization;
_plan = plan;
_additionalSeats = additionalSeats;
_previousSeats = organization.Seats ?? 0;
}

public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
Expand All @@ -63,22 +65,24 @@ public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription s

public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)
{

var item = SubscriptionItem(subscription, PlanIds.Single());
return new()
{
new SubscriptionItemOptions
{
Id = item?.Id,
Plan = PlanIds.Single(),
Quantity = _organization.Seats,
Deleted = item?.Id != null ? true : (bool?)null,
Quantity = _previousSeats,
Deleted = _previousSeats == 0 ? true : (bool?)null,
}
};
}
}

public class StorageSubscriptionUpdate : SubscriptionUpdate
{
private long? _prevStorage;
private readonly string _plan;
private readonly long? _additionalStorage;
protected override List<string> PlanIds => new() { _plan };
Expand All @@ -92,6 +96,7 @@ public StorageSubscriptionUpdate(string plan, long? additionalStorage)
public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
{
var item = SubscriptionItem(subscription, PlanIds.Single());
_prevStorage = item?.Quantity ?? 0;
return new()
{
new SubscriptionItemOptions
Expand All @@ -106,15 +111,20 @@ public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription s

public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)
{
if (!_prevStorage.HasValue)
{
throw new Exception("Unknown previous value, must first call UpgradeItemsOptions");
}

var item = SubscriptionItem(subscription, PlanIds.Single());
return new()
{
new SubscriptionItemOptions
{
Id = item?.Id,
Plan = _plan,
Quantity = item?.Quantity ?? 0,
Deleted = item?.Id != null ? true : (bool?)null,
Quantity = _prevStorage.Value,
Deleted = _prevStorage.Value == 0 ? true : (bool?)null,
}
};
}
Expand Down
90 changes: 49 additions & 41 deletions src/Core/Services/Implementations/StripePaymentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,8 @@ public async Task<string> PurchasePremiumAsync(User user, PaymentMethodType paym
private async Task<string> FinalizeSubscriptionChangeAsync(IStorableSubscriber storableSubscriber,
SubscriptionUpdate subscriptionUpdate, DateTime? prorationDate)
{
// remember, when in doubt, throw

var sub = await _stripeAdapter.SubscriptionGetAsync(storableSubscriber.GatewaySubscriptionId);
if (sub == null)
{
Expand Down Expand Up @@ -759,65 +761,71 @@ private async Task<string> FinalizeSubscriptionChangeAsync(IStorableSubscriber s
}
}

var subResponse = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, subUpdateOptions);

var invoice = await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new Stripe.InvoiceGetOptions());
if (invoice == null)
{
throw new BadRequestException("Unable to locate draft invoice for subscription update.");
}

string paymentIntentClientSecret = null;
if (invoice.AmountDue > 0 && updatedItemOptions.Any(i => i.Quantity > 0))
try
{
try
var subResponse = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, subUpdateOptions);

var invoice = await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new Stripe.InvoiceGetOptions());
if (invoice == null)
{
if (chargeNow)
throw new BadRequestException("Unable to locate draft invoice for subscription update.");
}

if (invoice.AmountDue > 0 && updatedItemOptions.Any(i => i.Quantity > 0))
{
try
{
paymentIntentClientSecret = await PayInvoiceAfterSubscriptionChangeAsync(
storableSubscriber, invoice);
if (chargeNow)
{
paymentIntentClientSecret = await PayInvoiceAfterSubscriptionChangeAsync(
storableSubscriber, invoice);
}
else
{
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId, new Stripe.InvoiceFinalizeOptions
{
AutoAdvance = false,
});
await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new Stripe.InvoiceSendOptions());
paymentIntentClientSecret = null;
}
}
else
catch
{
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId, new Stripe.InvoiceFinalizeOptions
// Need to revert the subscription
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
{
AutoAdvance = false,
Items = subscriptionUpdate.RevertItemsOptions(sub),
// This proration behavior prevents a false "credit" from
// being applied forward to the next month's invoice
ProrationBehavior = "none",
CollectionMethod = collectionMethod,
DaysUntilDue = daysUntilDue,
});
await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new Stripe.InvoiceSendOptions());
paymentIntentClientSecret = null;
throw;
}
}
catch
else if (!invoice.Paid)
{
// Pay invoice with no charge to customer this completes the invoice immediately without waiting the scheduled 1h
invoice = await _stripeAdapter.InvoicePayAsync(subResponse.LatestInvoiceId);
paymentIntentClientSecret = null;
}

}
finally
{
// Change back the subscription collection method and/or days until due
if (collectionMethod != "send_invoice" || daysUntilDue == null)
{
// Need to revert the subscription
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
{
Items = subscriptionUpdate.RevertItemsOptions(sub),
// This proration behavior prevents a false "credit" from
// being applied forward to the next month's invoice
ProrationBehavior = "none",
CollectionMethod = collectionMethod,
DaysUntilDue = daysUntilDue,
});
throw;
}
}
else if (!invoice.Paid)
{
// Pay invoice with no charge to customer this completes the invoice immediately without waiting the scheduled 1h
invoice = await _stripeAdapter.InvoicePayAsync(subResponse.LatestInvoiceId);
paymentIntentClientSecret = null;
}

// Change back the subscription collection method and/or days until due
if (collectionMethod != "send_invoice" || daysUntilDue == null)
{
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
{
CollectionMethod = collectionMethod,
DaysUntilDue = daysUntilDue,
});
}

return paymentIntentClientSecret;
}
Expand Down

0 comments on commit 610be2c

Please sign in to comment.