-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[AC-2361] Refactor StripeController (#4136)
* 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
1 parent
2657585
commit f045d06
Showing
18 changed files
with
1,705 additions
and
1,181 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
98
src/Billing/Services/Implementations/ChargeRefundedHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
67
src/Billing/Services/Implementations/ChargeSucceededHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
60
src/Billing/Services/Implementations/CustomerUpdatedHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
Oops, something went wrong.