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:
parent
810b653915
commit
052f760fbb
14
src/Billing/Constants/HandledStripeWebhook.cs
Normal file
14
src/Billing/Constants/HandledStripeWebhook.cs
Normal 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";
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user