1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

[fix] Cancel unpaid subscriptions (#2017)

* [refactor] Create a static class for documenting handled stripe webhooks

* [fix] Cancel unpaid subscriptions after 4 failed payments
This commit is contained in:
Addison Beck 2022-05-31 10:55:56 -04:00 committed by GitHub
parent 810b653915
commit 052f760fbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 62 additions and 14 deletions

View File

@ -0,0 +1,14 @@
namespace Bit.Billing.Constants
{
public static class HandledStripeWebhook
{
public static string SubscriptionDeleted => "customer.subscription.deleted";
public static string SubscriptionUpdated => "customer.subscriptions.updated";
public static string UpcomingInvoice => "invoice.upcoming";
public static string ChargeSucceeded => "charge.succeeded";
public static string ChargeRefunded => "charge.refunded";
public static string PaymentSucceeded => "invoice.payment_succeeded";
public static string PaymentFailed => "invoice.payment_failed";
public static string InvoiceCreated => "invoice.created";
}
}

View File

@ -54,7 +54,7 @@ namespace Bit.Billing.Controllers
try
{
var json = JsonSerializer.Serialize(JsonSerializer.Deserialize<JsonDocument>(body), JsonHelpers.Indented);
_logger.LogInformation(Constants.BypassFiltersEventId, "Apple IAP Notification:\n\n{0}", json);
_logger.LogInformation(Bit.Core.Constants.BypassFiltersEventId, "Apple IAP Notification:\n\n{0}", json);
return new OkResult();
}
catch (Exception e)

View File

@ -4,6 +4,7 @@ using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Bit.Billing.Constants;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
@ -113,8 +114,8 @@ namespace Bit.Billing.Controllers
return new BadRequestResult();
}
var subDeleted = parsedEvent.Type.Equals("customer.subscription.deleted");
var subUpdated = parsedEvent.Type.Equals("customer.subscription.updated");
var subDeleted = parsedEvent.Type.Equals(HandledStripeWebhook.SubscriptionDeleted);
var subUpdated = parsedEvent.Type.Equals(HandledStripeWebhook.SubscriptionUpdated);
if (subDeleted || subUpdated)
{
@ -159,7 +160,7 @@ namespace Bit.Billing.Controllers
}
}
}
else if (parsedEvent.Type.Equals("invoice.upcoming"))
else if (parsedEvent.Type.Equals(HandledStripeWebhook.UpcomingInvoice))
{
var invoice = await GetInvoiceAsync(parsedEvent);
var subscriptionService = new SubscriptionService();
@ -205,7 +206,7 @@ namespace Bit.Billing.Controllers
invoice.NextPaymentAttempt.Value, items, true);
}
}
else if (parsedEvent.Type.Equals("charge.succeeded"))
else if (parsedEvent.Type.Equals(HandledStripeWebhook.ChargeSucceeded))
{
var charge = await GetChargeAsync(parsedEvent);
var chargeTransaction = await _transactionRepository.GetByGatewayIdAsync(
@ -332,7 +333,7 @@ namespace Bit.Billing.Controllers
// Catch foreign key violations because user/org could have been deleted.
catch (SqlException e) when (e.Number == 547) { }
}
else if (parsedEvent.Type.Equals("charge.refunded"))
else if (parsedEvent.Type.Equals(HandledStripeWebhook.ChargeRefunded))
{
var charge = await GetChargeAsync(parsedEvent);
var chargeTransaction = await _transactionRepository.GetByGatewayIdAsync(
@ -382,7 +383,7 @@ namespace Bit.Billing.Controllers
_logger.LogWarning("Charge refund amount doesn't seem correct. " + charge.Id);
}
}
else if (parsedEvent.Type.Equals("invoice.payment_succeeded"))
else if (parsedEvent.Type.Equals(HandledStripeWebhook.PaymentSucceeded))
{
var invoice = await GetInvoiceAsync(parsedEvent, true);
if (invoice.Paid && invoice.BillingReason == "subscription_create")
@ -434,15 +435,11 @@ namespace Bit.Billing.Controllers
}
}
}
else if (parsedEvent.Type.Equals("invoice.payment_failed"))
else if (parsedEvent.Type.Equals(HandledStripeWebhook.PaymentFailed))
{
var invoice = await GetInvoiceAsync(parsedEvent, true);
if (!invoice.Paid && invoice.AttemptCount > 1 && UnpaidAutoChargeInvoiceForSubscriptionCycle(invoice))
{
await AttemptToPayInvoiceAsync(invoice);
}
await HandlePaymentFailed(await GetInvoiceAsync(parsedEvent, true));
}
else if (parsedEvent.Type.Equals("invoice.created"))
else if (parsedEvent.Type.Equals(HandledStripeWebhook.InvoiceCreated))
{
var invoice = await GetInvoiceAsync(parsedEvent, true);
if (!invoice.Paid && UnpaidAutoChargeInvoiceForSubscriptionCycle(invoice))
@ -804,5 +801,42 @@ namespace Bit.Billing.Controllers
private static bool IsSponsoredSubscription(Subscription subscription) =>
StaticStore.SponsoredPlans.Any(p => p.StripePlanId == subscription.Id);
private async Task HandlePaymentFailed(Invoice invoice)
{
if (!invoice.Paid && invoice.AttemptCount > 1 && UnpaidAutoChargeInvoiceForSubscriptionCycle(invoice))
{
// attempt count 4 = 11 days after initial failure
if (invoice.AttemptCount > 3)
{
await CancelSubscription(invoice.SubscriptionId);
await VoidOpenInvoices(invoice.SubscriptionId);
}
else
{
await AttemptToPayInvoiceAsync(invoice);
}
}
}
private async Task CancelSubscription(string subscriptionId)
{
await new SubscriptionService().CancelAsync(subscriptionId, new SubscriptionCancelOptions());
}
private async Task VoidOpenInvoices(string subscriptionId)
{
var invoiceService = new InvoiceService();
var options = new InvoiceListOptions
{
Status = "open",
Subscription = subscriptionId
};
var invoices = invoiceService.List(options);
foreach (var invoice in invoices)
{
await invoiceService.VoidInvoiceAsync(invoice.Id);
}
}
}
}