Browse Source

add some changes for paymentfailed handler

AC-1527-refactor-the-stripe-webhook-logic
Cy Okeke 2 years ago
parent
commit
02a32a4b2c
No known key found for this signature in database
GPG Key ID: B758F6C46EB146A2
  1. 43
      src/Billing/Services/Implementations/PaymentFailedHandler.cs
  2. 101
      src/Billing/Services/Implementations/PaymentSucceededHandler.cs
  3. 127
      src/Billing/Services/Implementations/StripeWebhookHandler.cs
  4. 6
      src/Billing/Utilities/StripeWebhookUtility.cs

43
src/Billing/Services/Implementations/PaymentFailedHandler.cs

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
using Bit.Billing.Constants;
using Microsoft.AspNetCore.Mvc;
using Stripe;
using Event = Stripe.Event;
namespace Bit.Billing.Services.Implementations;
public class PaymentFailedHandler : StripeWebhookHandler
{
private readonly IStripeEventService _stripeEventService;
public PaymentFailedHandler(IStripeEventService stripeEventService)
{
_stripeEventService = stripeEventService;
}
protected override bool CanHandle(Event parsedEvent)
{
return parsedEvent.Type.Equals(HandledStripeWebhook.PaymentSucceeded);
}
protected override async Task<IActionResult> ProcessEvent(Event parsedEvent)
{
await HandlePaymentFailedAsync(await _stripeEventService.GetInvoice(parsedEvent, true));
return new OkResult();
}
private async Task HandlePaymentFailedAsync(Invoice invoice)
{
if (!invoice.Paid && invoice.AttemptCount > 1 && UnpaidAutoChargeInvoiceForSubscriptionCycle(invoice))
{
var subscriptionService = new SubscriptionService();
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
// attempt count 4 = 11 days after initial failure
if (invoice.AttemptCount <= 3 ||
!subscription.Items.Any(i => i.Price.Id is PremiumPlanId or PremiumPlanIdAppStore))
{
await AttemptToPayInvoiceAsync(invoice);
}
}
}
}

101
src/Billing/Services/Implementations/PaymentSucceededHandler.cs

@ -0,0 +1,101 @@ @@ -0,0 +1,101 @@
using Bit.Billing.Constants;
using Bit.Core.Context;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Mvc;
using Stripe;
using Event = Stripe.Event;
namespace Bit.Billing.Services.Implementations;
public class PaymentSucceededHandler : StripeWebhookHandler
{
private readonly IStripeEventService _stripeEventService;
private readonly IOrganizationService _organizationService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext;
private readonly IUserService _userService;
private readonly IUserRepository _userRepository;
public PaymentSucceededHandler(IStripeEventService stripeEventService,
IOrganizationService organizationService,
IOrganizationRepository organizationRepository,
IReferenceEventService referenceEventService,
ICurrentContext currentContext,
IUserService userService,
IUserRepository userRepository)
{
_stripeEventService = stripeEventService;
_organizationService = organizationService;
_organizationRepository = organizationRepository;
_referenceEventService = referenceEventService;
_currentContext = currentContext;
_userService = userService;
_userRepository = userRepository;
}
protected override bool CanHandle(Event parsedEvent)
{
return parsedEvent.Type.Equals(HandledStripeWebhook.PaymentSucceeded);
}
protected override async Task<IActionResult> ProcessEvent(Event parsedEvent)
{
var invoice = await _stripeEventService.GetInvoice(parsedEvent, true);
if (invoice.Paid && invoice.BillingReason == "subscription_create")
{
var subscriptionService = new SubscriptionService();
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
if (subscription?.Status == StripeSubscriptionStatus.Active)
{
if (DateTime.UtcNow - invoice.Created < TimeSpan.FromMinutes(1))
{
await Task.Delay(5000);
}
var ids = GetIdsFromMetaData(subscription.Metadata);
// org
if (ids.Item1.HasValue)
{
if (subscription.Items.Any(i =>
StaticStore.Plans.Any(p => p.PasswordManager.StripePlanId == i.Plan.Id)))
{
await _organizationService.EnableAsync(ids.Item1.Value, subscription.CurrentPeriodEnd);
var organization = await _organizationRepository.GetByIdAsync(ids.Item1.Value);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.Rebilled, organization, _currentContext)
{
PlanName = organization?.Plan,
PlanType = organization?.PlanType,
Seats = organization?.Seats,
Storage = organization?.MaxStorageGb,
});
}
}
// user
else if (ids.Item2.HasValue)
{
if (subscription.Items.Any(i => i.Plan.Id == PremiumPlanId))
{
await _userService.EnablePremiumAsync(ids.Item2.Value, subscription.CurrentPeriodEnd);
var user = await _userRepository.GetByIdAsync(ids.Item2.Value);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.Rebilled, user, _currentContext)
{
PlanName = PremiumPlanId,
Storage = user?.MaxStorageGb,
});
}
}
}
}
return new OkResult();
}
}

127
src/Billing/Services/Implementations/StripeWebhookHandler.cs

@ -30,7 +30,10 @@ public abstract class StripeWebhookHandler @@ -30,7 +30,10 @@ public abstract class StripeWebhookHandler
}
}
public Tuple<Guid?, Guid?> GetIdsFromMetaData(IDictionary<string, string> metaData)
protected abstract bool CanHandle(Event parsedEvent);
protected abstract Task<IActionResult> ProcessEvent(Event parsedEvent);
public static Tuple<Guid?, Guid?> GetIdsFromMetaData(IDictionary<string, string> metaData)
{
if (metaData == null || !metaData.Any())
{
@ -72,7 +75,125 @@ public abstract class StripeWebhookHandler @@ -72,7 +75,125 @@ public abstract class StripeWebhookHandler
public static bool IsSponsoredSubscription(Subscription subscription) =>
StaticStore.SponsoredPlans.Any(p => p.StripePlanId == subscription.Id);
protected abstract bool CanHandle(Event parsedEvent);
protected abstract Task<IActionResult> ProcessEvent(Event parsedEvent);
public static bool UnpaidAutoChargeInvoiceForSubscriptionCycle(Invoice invoice)
{
return invoice.AmountDue > 0 && !invoice.Paid && invoice.CollectionMethod == "charge_automatically" &&
invoice.BillingReason == "subscription_cycle" && invoice.SubscriptionId != null;
}
public async Task<bool> AttemptToPayInvoiceAsync(Invoice invoice, bool attemptToPayWithStripe = false)
{
var customerService = new CustomerService();
var customer = await customerService.GetAsync(invoice.CustomerId);
if (customer?.Metadata?.ContainsKey("appleReceipt") ?? false)
{
return await AttemptToPayInvoiceWithAppleReceiptAsync(invoice, customer);
}
if (customer?.Metadata?.ContainsKey("btCustomerId") ?? false)
{
return await AttemptToPayInvoiceWithBraintreeAsync(invoice, customer);
}
if (attemptToPayWithStripe)
{
return await AttemptToPayInvoiceWithStripeAsync(invoice);
}
return false;
}
private async Task<bool> AttemptToPayInvoiceWithAppleReceiptAsync(Invoice invoice, Customer customer)
{
if (!customer?.Metadata?.ContainsKey("appleReceipt") ?? true)
{
return false;
}
var originalAppleReceiptTransactionId = customer.Metadata["appleReceipt"];
var appleReceiptRecord = await _appleIapService.GetReceiptAsync(originalAppleReceiptTransactionId);
if (string.IsNullOrWhiteSpace(appleReceiptRecord?.Item1) || !appleReceiptRecord.Item2.HasValue)
{
return false;
}
var subscriptionService = new SubscriptionService();
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
var ids = GetIdsFromMetaData(subscription?.Metadata);
if (!ids.Item2.HasValue)
{
// Apple receipt is only for user subscriptions
return false;
}
if (appleReceiptRecord.Item2.Value != ids.Item2.Value)
{
_logger.LogError("User Ids for Apple Receipt and subscription do not match: {0} != {1}.",
appleReceiptRecord.Item2.Value, ids.Item2.Value);
return false;
}
var appleReceiptStatus = await _appleIapService.GetVerifiedReceiptStatusAsync(appleReceiptRecord.Item1);
if (appleReceiptStatus == null)
{
// TODO: cancel sub if receipt is cancelled?
return false;
}
var receiptExpiration = appleReceiptStatus.GetLastExpiresDate().GetValueOrDefault(DateTime.MinValue);
var invoiceDue = invoice.DueDate.GetValueOrDefault(DateTime.MinValue);
if (receiptExpiration <= invoiceDue)
{
_logger.LogWarning("Apple receipt expiration is before invoice due date. {0} <= {1}",
receiptExpiration, invoiceDue);
return false;
}
var receiptLastTransactionId = appleReceiptStatus.GetLastTransactionId();
var existingTransaction = await _transactionRepository.GetByGatewayIdAsync(
GatewayType.AppStore, receiptLastTransactionId);
if (existingTransaction != null)
{
_logger.LogWarning("There is already an existing transaction for this Apple receipt.",
receiptLastTransactionId);
return false;
}
var appleTransaction = appleReceiptStatus.BuildTransactionFromLastTransaction(
PremiumPlanAppleIapPrice, ids.Item2.Value);
appleTransaction.Type = TransactionType.Charge;
var invoiceService = new InvoiceService();
try
{
await invoiceService.UpdateAsync(invoice.Id, new InvoiceUpdateOptions
{
Metadata = new Dictionary<string, string>
{
["appleReceipt"] = appleReceiptStatus.GetOriginalTransactionId(),
["appleReceiptTransactionId"] = receiptLastTransactionId
}
});
await _transactionRepository.CreateAsync(appleTransaction);
await invoiceService.PayAsync(invoice.Id, new InvoicePayOptions { PaidOutOfBand = true });
}
catch (Exception e)
{
if (e.Message.Contains("Invoice is already paid"))
{
await invoiceService.UpdateAsync(invoice.Id, new InvoiceUpdateOptions
{
Metadata = invoice.Metadata
});
}
else
{
throw;
}
}
return true;
}
}

6
src/Billing/Utilities/StripeWebhookUtility.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
namespace Bit.Billing.Utilities;
public static class StripeWebhookUtility
{
}
Loading…
Cancel
Save