Skip to content

Commit

Permalink
[AC-2361] Refactor StripeController (#4136)
Browse files Browse the repository at this point in the history
* Changes ensures provider_id is handled and stored for Braintree.

Signed-off-by: Cy Okeke <[email protected]>

* refactoring of the stripeController class

Signed-off-by: Cy Okeke <[email protected]>

* Move the constant variables to utility class

Signed-off-by: Cy Okeke <[email protected]>

* Adding comments to the methods

Signed-off-by: Cy Okeke <[email protected]>

* Add more comments to describe the method

Signed-off-by: Cy Okeke <[email protected]>

* Add the providerId changes

Signed-off-by: Cy Okeke <[email protected]>

* Add the missing providerId

Signed-off-by: Cy Okeke <[email protected]>

* Fix the IsSponsoredSubscription bug

Signed-off-by: Cy Okeke <[email protected]>

---------

Signed-off-by: Cy Okeke <[email protected]>
  • Loading branch information
cyprain-okeke authored Jun 26, 2024
1 parent 2657585 commit f045d06
Show file tree
Hide file tree
Showing 18 changed files with 1,705 additions and 1,181 deletions.
1,193 changes: 12 additions & 1,181 deletions src/Billing/Controllers/StripeController.cs

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions src/Billing/Services/IStripeEventProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Event = Stripe.Event;
namespace Bit.Billing.Services;

public interface IStripeEventProcessor
{
/// <summary>
/// Processes the specified Stripe event asynchronously.
/// </summary>
/// <param name="parsedEvent">The Stripe event to be processed.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task ProcessEventAsync(Event parsedEvent);
}
67 changes: 67 additions & 0 deletions src/Billing/Services/IStripeEventUtilityService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Stripe;
using Transaction = Bit.Core.Entities.Transaction;
namespace Bit.Billing.Services;

public interface IStripeEventUtilityService
{
/// <summary>
/// Gets the organization or user ID from the metadata of a Stripe Charge object.
/// </summary>
/// <param name="charge"></param>
/// <returns></returns>
Task<(Guid?, Guid?, Guid?)> GetEntityIdsFromChargeAsync(Charge charge);

/// <summary>
/// Gets the organizationId, userId, or providerId from the metadata of a Stripe Subscription object.
/// </summary>
/// <param name="metadata"></param>
/// <returns></returns>
Tuple<Guid?, Guid?, Guid?> GetIdsFromMetadata(Dictionary<string, string> metadata);

/// <summary>
/// Determines whether the specified subscription is a sponsored subscription.
/// </summary>
/// <param name="subscription">The subscription to be evaluated.</param>
/// <returns>
/// A boolean value indicating whether the subscription is a sponsored subscription.
/// Returns <c>true</c> if the subscription matches any of the sponsored plans; otherwise, <c>false</c>.
/// </returns>
bool IsSponsoredSubscription(Subscription subscription);

/// <summary>
/// Converts a Stripe Charge object to a Bitwarden Transaction object.
/// </summary>
/// <param name="charge"></param>
/// <param name="organizationId"></param>
/// <param name="userId"></param>
/// /// <param name="providerId"></param>
/// <returns></returns>
Transaction FromChargeToTransaction(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId);

/// <summary>
/// Attempts to pay the specified invoice. If a customer is eligible, the invoice is paid using Braintree or Stripe.
/// </summary>
/// <param name="invoice">The invoice to be paid.</param>
/// <param name="attemptToPayWithStripe">Indicates whether to attempt payment with Stripe. Defaults to false.</param>
/// <returns>A task representing the asynchronous operation. The task result contains a boolean value indicating whether the invoice payment attempt was successful.</returns>
Task<bool> AttemptToPayInvoiceAsync(Invoice invoice, bool attemptToPayWithStripe = false);


/// <summary>
/// Determines whether an invoice should be attempted to be paid based on certain criteria.
/// </summary>
/// <param name="invoice">The invoice to be evaluated.</param>
/// <returns>A boolean value indicating whether the invoice should be attempted to be paid.</returns>
bool ShouldAttemptToPayInvoice(Invoice invoice);

/// <summary>
/// The ID for the premium annual plan.
/// </summary>
const string PremiumPlanId = "premium-annually";

/// <summary>
/// The ID for the premium annual plan via the App Store.
/// </summary>
const string PremiumPlanIdAppStore = "premium-annually-app";

}
67 changes: 67 additions & 0 deletions src/Billing/Services/IStripeWebhookHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Event = Stripe.Event;
namespace Bit.Billing.Services;

public interface IStripeWebhookHandler
{
/// <summary>
/// Handles the specified Stripe event asynchronously.
/// </summary>
/// <param name="parsedEvent">The Stripe event to be handled.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task HandleAsync(Event parsedEvent);
}

/// <summary>
/// Defines the contract for handling Stripe subscription deleted events.
/// </summary>
public interface ISubscriptionDeletedHandler : IStripeWebhookHandler;

/// <summary>
/// Defines the contract for handling Stripe subscription updated events.
/// </summary>
public interface ISubscriptionUpdatedHandler : IStripeWebhookHandler;

/// <summary>
/// Defines the contract for handling Stripe upcoming invoice events.
/// </summary>
public interface IUpcomingInvoiceHandler : IStripeWebhookHandler;

/// <summary>
/// Defines the contract for handling Stripe charge succeeded events.
/// </summary>
public interface IChargeSucceededHandler : IStripeWebhookHandler;

/// <summary>
/// Defines the contract for handling Stripe charge refunded events.
/// </summary>
public interface IChargeRefundedHandler : IStripeWebhookHandler;

/// <summary>
/// Defines the contract for handling Stripe payment succeeded events.
/// </summary>
public interface IPaymentSucceededHandler : IStripeWebhookHandler;

/// <summary>
/// Defines the contract for handling Stripe payment failed events.
/// </summary>
public interface IPaymentFailedHandler : IStripeWebhookHandler;

/// <summary>
/// Defines the contract for handling Stripe invoice created events.
/// </summary>
public interface IInvoiceCreatedHandler : IStripeWebhookHandler;

/// <summary>
/// Defines the contract for handling Stripe payment method attached events.
/// </summary>
public interface IPaymentMethodAttachedHandler : IStripeWebhookHandler;

/// <summary>
/// Defines the contract for handling Stripe customer updated events.
/// </summary>
public interface ICustomerUpdatedHandler : IStripeWebhookHandler;

/// <summary>
/// Defines the contract for handling Stripe Invoice Finalized events.
/// </summary>
public interface IInvoiceFinalizedHandler : IStripeWebhookHandler;
98 changes: 98 additions & 0 deletions src/Billing/Services/Implementations/ChargeRefundedHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using Bit.Billing.Constants;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Microsoft.Data.SqlClient;
using Event = Stripe.Event;
using Transaction = Bit.Core.Entities.Transaction;
using TransactionType = Bit.Core.Enums.TransactionType;
namespace Bit.Billing.Services.Implementations;

public class ChargeRefundedHandler : IChargeRefundedHandler
{
private readonly ILogger<ChargeRefundedHandler> _logger;
private readonly IStripeEventService _stripeEventService;
private readonly ITransactionRepository _transactionRepository;
private readonly IStripeEventUtilityService _stripeEventUtilityService;

public ChargeRefundedHandler(
ILogger<ChargeRefundedHandler> logger,
IStripeEventService stripeEventService,
ITransactionRepository transactionRepository,
IStripeEventUtilityService stripeEventUtilityService)
{
_logger = logger;
_stripeEventService = stripeEventService;
_transactionRepository = transactionRepository;
_stripeEventUtilityService = stripeEventUtilityService;
}

/// <summary>
/// Handles the <see cref="HandledStripeWebhook.ChargeRefunded"/> event type from Stripe.
/// </summary>
/// <param name="parsedEvent"></param>
public async Task HandleAsync(Event parsedEvent)
{
var charge = await _stripeEventService.GetCharge(parsedEvent, true, ["refunds"]);
var parentTransaction = await _transactionRepository.GetByGatewayIdAsync(GatewayType.Stripe, charge.Id);
if (parentTransaction == null)
{
// Attempt to create a transaction for the charge if it doesn't exist
var (organizationId, userId, providerId) = await _stripeEventUtilityService.GetEntityIdsFromChargeAsync(charge);
var tx = _stripeEventUtilityService.FromChargeToTransaction(charge, organizationId, userId, providerId);
try
{
parentTransaction = await _transactionRepository.CreateAsync(tx);
}
catch (SqlException e) when (e.Number == 547) // FK constraint violation
{
_logger.LogWarning(
"Charge refund could not create transaction as entity may have been deleted. {ChargeId}",
charge.Id);
return;
}
}

var amountRefunded = charge.AmountRefunded / 100M;

if (parentTransaction.Refunded.GetValueOrDefault() ||
parentTransaction.RefundedAmount.GetValueOrDefault() >= amountRefunded)
{
_logger.LogWarning(
"Charge refund amount doesn't match parent transaction's amount or parent has already been refunded. {ChargeId}",
charge.Id);
return;
}

parentTransaction.RefundedAmount = amountRefunded;
if (charge.Refunded)
{
parentTransaction.Refunded = true;
}

await _transactionRepository.ReplaceAsync(parentTransaction);

foreach (var refund in charge.Refunds)
{
var refundTransaction = await _transactionRepository.GetByGatewayIdAsync(
GatewayType.Stripe, refund.Id);
if (refundTransaction != null)
{
continue;
}

await _transactionRepository.CreateAsync(new Transaction
{
Amount = refund.Amount / 100M,
CreationDate = refund.Created,
OrganizationId = parentTransaction.OrganizationId,
UserId = parentTransaction.UserId,
ProviderId = parentTransaction.ProviderId,
Type = TransactionType.Refund,
Gateway = GatewayType.Stripe,
GatewayId = refund.Id,
PaymentMethodType = parentTransaction.PaymentMethodType,
Details = parentTransaction.Details
});
}
}
}
67 changes: 67 additions & 0 deletions src/Billing/Services/Implementations/ChargeSucceededHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Bit.Billing.Constants;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Microsoft.Data.SqlClient;
using Event = Stripe.Event;

namespace Bit.Billing.Services.Implementations;

public class ChargeSucceededHandler : IChargeSucceededHandler
{
private readonly ILogger<ChargeSucceededHandler> _logger;
private readonly IStripeEventService _stripeEventService;
private readonly ITransactionRepository _transactionRepository;
private readonly IStripeEventUtilityService _stripeEventUtilityService;

public ChargeSucceededHandler(
ILogger<ChargeSucceededHandler> logger,
IStripeEventService stripeEventService,
ITransactionRepository transactionRepository,
IStripeEventUtilityService stripeEventUtilityService)
{
_logger = logger;
_stripeEventService = stripeEventService;
_transactionRepository = transactionRepository;
_stripeEventUtilityService = stripeEventUtilityService;
}

/// <summary>
/// Handles the <see cref="HandledStripeWebhook.ChargeSucceeded"/> event type from Stripe.
/// </summary>
/// <param name="parsedEvent"></param>
public async Task HandleAsync(Event parsedEvent)
{
var charge = await _stripeEventService.GetCharge(parsedEvent);
var existingTransaction = await _transactionRepository.GetByGatewayIdAsync(GatewayType.Stripe, charge.Id);
if (existingTransaction is not null)
{
_logger.LogInformation("Charge success already processed. {ChargeId}", charge.Id);
return;
}

var (organizationId, userId, providerId) = await _stripeEventUtilityService.GetEntityIdsFromChargeAsync(charge);
if (!organizationId.HasValue && !userId.HasValue && !providerId.HasValue)
{
_logger.LogWarning("Charge success has no subscriber ids. {ChargeId}", charge.Id);
return;
}

var transaction = _stripeEventUtilityService.FromChargeToTransaction(charge, organizationId, userId, providerId);
if (!transaction.PaymentMethodType.HasValue)
{
_logger.LogWarning("Charge success from unsupported source/method. {ChargeId}", charge.Id);
return;
}

try
{
await _transactionRepository.CreateAsync(transaction);
}
catch (SqlException e) when (e.Number == 547)
{
_logger.LogWarning(
"Charge success could not create transaction as entity may have been deleted. {ChargeId}",
charge.Id);
}
}
}
60 changes: 60 additions & 0 deletions src/Billing/Services/Implementations/CustomerUpdatedHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Bit.Core.Context;
using Bit.Core.Repositories;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Event = Stripe.Event;

namespace Bit.Billing.Services.Implementations;

public class CustomerUpdatedHandler : ICustomerUpdatedHandler
{
private readonly IOrganizationRepository _organizationRepository;
private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext;
private readonly IStripeEventService _stripeEventService;
private readonly IStripeEventUtilityService _stripeEventUtilityService;

public CustomerUpdatedHandler(
IOrganizationRepository organizationRepository,
IReferenceEventService referenceEventService,
ICurrentContext currentContext,
IStripeEventService stripeEventService,
IStripeEventUtilityService stripeEventUtilityService)
{
_organizationRepository = organizationRepository;
_referenceEventService = referenceEventService;
_currentContext = currentContext;
_stripeEventService = stripeEventService;
_stripeEventUtilityService = stripeEventUtilityService;
}

/// <summary>
/// Handles the <see cref="HandledStripeWebhook.CustomerUpdated"/> event type from Stripe.
/// </summary>
/// <param name="parsedEvent"></param>
public async Task HandleAsync(Event parsedEvent)
{
var customer = await _stripeEventService.GetCustomer(parsedEvent, true, ["subscriptions"]);
if (customer.Subscriptions == null || !customer.Subscriptions.Any())
{
return;
}

var subscription = customer.Subscriptions.First();

var (organizationId, _, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);

if (!organizationId.HasValue)
{
return;
}

var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
organization.BillingEmail = customer.Email;
await _organizationRepository.ReplaceAsync(organization);

await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.OrganizationEditedInStripe, organization, _currentContext));
}
}
Loading

0 comments on commit f045d06

Please sign in to comment.